Introducción
Para los que no lo sepáis, próximamente se estrena la película Prometheus en Estados Unidos y desde Plain Concepts hemos desarrollado el training center del sitio web. Se puede acceder desde este enlace: http://www.projectprometheus.com/trainingcenter/. El proyecto ha estado financiado por Microsoft, más concretamente por el equipo de Internet Explorer, así que como página web que es, se ha desarrollado utilizando las últimas tecnologías web: HTML5 + CSS3.
Training center
El centro de entrenamiento es un sitio web donde los candidatos al proyecto Prometheus, de la empresa Weyland, puede probar su valía. El entrenamiento cuenta con 5 pruebas (juegos) que el recluta tiene que completar en un tiempo determinado. Una vez superadas las cinco pruebas el recluta puede formar parte de Weyland Industries. Los cinco juegos han sido desarrollados por gente de Plain Concepts:
-
Jesus David Garcia
-
Fernando
-
Luis Guerrero
Cada uno de los cuales ha desarrollado uno de los juegos del centro de entrenamiento. En mi caso he desarrollado el cubo de rubick en 2 y 3 dimensiones.
Aquí se puede ver una captura del juego en Internet Explorer llamado Prefrontal Cortex.
HTML5 / Javascript
Todos los juegos han sido desarrollados en HTML5 utilizando JavaScript para la parte de programación, en mi caso he utilizado Canvas para dibujar los cubos. Eso significa que todos los juegos funcionan perfectamente en todos los navegadores modernos, incluyendo Google Chrome, Firefox, Safari, Opera y Internet Explorer 9 y 10.
Para el desarrollo de los juegos se creo un motor en JavaScript que nos permitiera dibujar en un Canvas la geometría de los modelos de los cubos. Este motor no utiliza WebGL para renderizar los cubos, porque Internet Explorer no tiene soporte (además de en el resto es experimental), por lo que se opto por hacer un motor grafico completo desde cero. Es decir, todo el pipeline de grafica se tiene que hacer en JavaScript, esto significa, entre otras cosas, que tenemos que emular por software como funciona una tarjeta gráfica y eso normalmente es más lento que el propio hardware. Así que el desafío de implementar un pipeline gráfico por software es mayor ya que tiene que tener un rendimiento aceptable.
JavaScript 101
JavaScript es un lenguaje en el que existen varios tipos de datos básicos con los que podemos trabajar.
-
Object
-
Array
-
Number
-
String
-
Boolean
Los objetos son la forma más común a la hora de trabajar en JavaScript y se pueden utilizar de muchas maneras.
La forma más sencilla de crear un objeto es:
1: var myObject = {}
A partir de ahí se pueden ir agregando propiedades al objeto sin ningún tipo de restricción. No son propiedades como las que se puede estar acostumbrado en C#, sino que el tipo object se comporta como una especie de diccionario de pares nombre valor.
Se pueden crear propiedades de la siguiente manera a un objeto previamente definido.
1: myObject.name = ‘Luis’;
2: myObject.number = 42;
Así el objeto pasará a tener dos propiedades, una llamada ‘name’ con el valor ‘Luis’ y otra llamada ‘number’ con el valor ‘42’.
También se puede acceder a esas propiedades como si de un diccionario se tratase. Las dos formas son igual de válidas y correctas.
1: var name = myObject[‘name’];
Después de la ejecución de esta línea de código lo que se establece en la variable name es el valor de ‘Luis’, previamente establecido.
Con esta nueva forma de acceder a las propiedades no solo se pueden leer valores almacenados en un objeto sino que también se pueden guardar.
myObject[‘currentDate’] = new Date();
Vector3
Como ejemplo de objeto se va a definir Vector3; un vector de 3 dimensiones.
var vector3 = {x: 1, y: 1, z: 1}
¿Cuál es el problema con este Vector3?
El rendimiento. Como se ha dicho antes todos los objetos en JavaScript se comportan como un diccionario de pares nombre / valor, así que en cada una de las operaciones en las que se tenga que leer o escribir el valor de x, y o z, el runtime de JavaScript tiene que comprobar que la propiedad existe o no y luego leerla o almacenarla. Todo esto lleva tiempo. Es como si se programase todo el acceso a propiedades y campos en .NET utilizando únicamente la API de Reflexion (System.Reflection).
En el caso del motor de 3D en JavaScript Vector2, Vector3, Vector4, Color y Matrix son tipos que se están usando constantemente para dibujar la geometría de los cubos, así que esos tipos fueron los primero en ser optimizados.
La solución por la que se opto fue eliminar la definición de los tipos, es decir, que por ejemplo Vector2, Vector3, Vector4 y Color pasaron a ser un array de 2, 3, 4 y 4 posiciones respectivamente. Así que por convención lo que se hizo fue que la posición dentro del array representaba una coordenada de las dimensiones del vector.
-
X: array[0]
-
Y: array[1]
-
Z: array[2]
-
W: arrat[3]
En el caso de Matrix que se tenía M11, M12…M21,M22..M31..M44 pasaron a ser también las posiciones de un array.
Veamos como se ha cambiado la multiplicación de matrices, uno de los cuellos de botella, en cuanto a rendimiento se refiere.
Antes
1: function Multiply(matrix1, matrix2) {
2: var matrix = new Matrix();
3: matrix.M11 = (((matrix1.M11 * matrix2.M11) + (matrix1.M12 * matrix2.M21)) + (matrix1.M13 * matrix2.M31)) + (matrix1.M14 * matrix2.M41);
4: matrix.M12 = (((matrix1.M11 * matrix2.M12) + (matrix1.M12 * matrix2.M22)) + (matrix1.M13 * matrix2.M32)) + (matrix1.M14 * matrix2.M42);
5: matrix.M13 = (((matrix1.M11 * matrix2.M13) + (matrix1.M12 * matrix2.M23)) + (matrix1.M13 * matrix2.M33)) + (matrix1.M14 * matrix2.M43);
6: matrix.M14 = (((matrix1.M11 * matrix2.M14) + (matrix1.M12 * matrix2.M24)) + (matrix1.M13 * matrix2.M34)) + (matrix1.M14 * matrix2.M44);
7: matrix.M21 = (((matrix1.M21 * matrix2.M11) + (matrix1.M22 * matrix2.M21)) + (matrix1.M23 * matrix2.M31)) + (matrix1.M24 * matrix2.M41);
8: matrix.M22 = (((matrix1.M21 * matrix2.M12) + (matrix1.M22 * matrix2.M22)) + (matrix1.M23 * matrix2.M32)) + (matrix1.M24 * matrix2.M42);
9: matrix.M23 = (((matrix1.M21 * matrix2.M13) + (matrix1.M22 * matrix2.M23)) + (matrix1.M23 * matrix2.M33)) + (matrix1.M24 * matrix2.M43);
10: matrix.M24 = (((matrix1.M21 * matrix2.M14) + (matrix1.M22 * matrix2.M24)) + (matrix1.M23 * matrix2.M34)) + (matrix1.M24 * matrix2.M44);
11: matrix.M31 = (((matrix1.M31 * matrix2.M11) + (matrix1.M32 * matrix2.M21)) + (matrix1.M33 * matrix2.M31)) + (matrix1.M34 * matrix2.M41);
12: matrix.M32 = (((matrix1.M31 * matrix2.M12) + (matrix1.M32 * matrix2.M22)) + (matrix1.M33 * matrix2.M32)) + (matrix1.M34 * matrix2.M42);
13: matrix.M33 = (((matrix1.M31 * matrix2.M13) + (matrix1.M32 * matrix2.M23)) + (matrix1.M33 * matrix2.M33)) + (matrix1.M34 * matrix2.M43);
14: matrix.M34 = (((matrix1.M31 * matrix2.M14) + (matrix1.M32 * matrix2.M24)) + (matrix1.M33 * matrix2.M34)) + (matrix1.M34 * matrix2.M44);
15: matrix.M41 = (((matrix1.M41 * matrix2.M11) + (matrix1.M42 * matrix2.M21)) + (matrix1.M43 * matrix2.M31)) + (matrix1.M44 * matrix2.M41);
16: matrix.M42 = (((matrix1.M41 * matrix2.M12) + (matrix1.M42 * matrix2.M22)) + (matrix1.M43 * matrix2.M32)) + (matrix1.M44 * matrix2.M42);
17: matrix.M43 = (((matrix1.M41 * matrix2.M13) + (matrix1.M42 * matrix2.M23)) + (matrix1.M43 * matrix2.M33)) + (matrix1.M44 * matrix2.M43);
18: matrix.M44 = (((matrix1.M41 * matrix2.M14) + (matrix1.M42 * matrix2.M24)) + (matrix1.M43 * matrix2.M34)) + (matrix1.M44 * matrix2.M44);
19: return matrix;
20: }
La multiplicación simplemente accedía a cada uno de los índices de la matriz, los multiplicaba y luego los asignada de vuelta a la matriz de resultado. Como hemos dicho antes, esto implica leer una gran cantidad de propiedades durante el dibujado de un frame de la escena.
Ahora
1: function Multiply(matrix1, matrix2) {
2: var matrix = new Matrix();
3: var position = matrix.position;
4: var position1 = matrix1.position;
5: var position2 = matrix2.position;
6: position[0] = (((position1[0] * position2[0]) + (position1[1] * position2[4])) + (position1[2] * position2[8])) + (position1[3] * position2[12]);
7: position[1] = (((position1[0] * position2[1]) + (position1[1] * position2[5])) + (position1[2] * position2[9])) + (position1[3] * position2[13]);
8: position[2] = (((position1[0] * position2[2]) + (position1[1] * position2[6])) + (position1[2] * position2[10])) + (position1[3] * position2[14]);
9: position[3] = (((position1[0] * position2[3]) + (position1[1] * position2[7])) + (position1[2] * position2[11])) + (position1[3] * position2[15]);
10: position[4] = (((position1[4] * position2[0]) + (position1[5] * position2[4])) + (position1[6] * position2[8])) + (position1[7] * position2[12]);
11: position[5] = (((position1[4] * position2[1]) + (position1[5] * position2[5])) + (position1[6] * position2[9])) + (position1[7] * position2[13]);
12: position[6] = (((position1[4] * position2[2]) + (position1[5] * position2[6])) + (position1[6] * position2[10])) + (position1[7] * position2[14]);
13: position[7] = (((position1[4] * position2[3]) + (position1[5] * position2[7])) + (position1[6] * position2[11])) + (position1[7] * position2[15]);
14: position[8] = (((position1[8] * position2[0]) + (position1[9] * position2[4])) + (position1[10] * position2[8])) + (position1[11] * position2[12]);
15: position[9] = (((position1[8] * position2[1]) + (position1[9] * position2[5])) + (position1[10] * position2[9])) + (position1[11] * position2[13]);
16: position[10] = (((position1[8] * position2[2]) + (position1[9] * position2[6])) + (position1[10] * position2[10])) + (position1[11] * position2[14]);
17: position[11] = (((position1[8] * position2[3]) + (position1[9] * position2[7])) + (position1[10] * position2[11])) + (position1[11] * position2[15]);
18: position[12] = (((position1[12] * position2[0]) + (position1[13] * position2[4])) + (position1[14] * position2[8])) + (position1[15] * position2[12]);
19: position[13] = (((position1[12] * position2[1]) + (position1[13] * position2[5])) + (position1[14] * position2[9])) + (position1[15] * position2[13]);
20: position[14] = (((position1[12] * position2[2]) + (position1[13] * position2[6])) + (position1[14] * position2[10])) + (position1[15] * position2[14]);
21: position[15] = (((position1[12] * position2[3]) + (position1[13] * position2[7])) + (position1[14] * position2[11])) + (position1[15] * position2[15]);
22: return matrix;
23: }
Lo primero que se observa es que el código pasa a ser más críptico que el anterior, es decir, que ahora únicamente se tiene son los diferentes índices de position dentro de tres arrays, que representan las tres matrices con las que se esta trabajando en este momento.
Así que se ha pasado de,
matrix.M11 = (((matrix1.M11 * matrix2.M11) + (matrix1.M12 * matrix2.M21)) +
(matrix1.M13 * matrix2.M31)) + (matrix1.M14 * matrix2.M41);
a esto:
position[0] = (((position1[0] * position2[0]) + (position1[1] * position2[4])) +
(position1[2] * position2[8])) + (position1[3] * position2[12]);
Ya que ahora todas las posiciones de la matriz están almacenadas en un array de 16 posiciones lo que se tiene que hacer si se quiere acceder al valor M11 es acceder a la posición 0 de array, en el caso del valor M31 a la posición 8 de array y así sucesivamente.
Otras optimizaciones
Tamaño de los arrays
Si se tiene un array que tiene una propiedad length por la que se quiere iterar para realizar una acción por cada uno de los elementos del array, es recomendable no poner directamente el valor de myArray.length para comprobar si se ha llegado al final de array, sino guardar el tamaño del array en una variable y usar esta variable.
1: var myArray = new Array();
2:
3: for (var i = 0; i < myArray.length; i++) {
4: myArray[i] = i;
5: }
6:
7: var length = myArray.length;
8: for (var i = 0; i < length; i++) {
9: myArray[i] = i;
10: }
Cachear variables
Si durante la ejecución de un método se tiene variables que vamos a usar y estas variables son propiedades de un objeto, es mejor definirlas como variables en el ámbito del método que no referenciarlas desde el objeto original.
1: var myObject =
2: {
3: name: 'luis',
4: company: {
5: name: 'PlainConcepts',
6: location: 'address'
7: }
8: };
9:
10: var companyAddress = myObject.company.location;
11: var company = myObject.company;
12: companyAddress = company.location;
Espero que estas notas sobre optimización de JavaScript os sean útiles.
Luis Guerrero.
Muy interesante, Luis.
¿Habéis usado algún profiler para saber qué optimizar o habéis tenido que hacerlo a ojo?
Hola Juanma, pues en principio hemos usado el profiler que viene con Internet Explorer en las herramientas de desarrollador y con el profiler de Google Chrome.
Eso lo que te mide son las ejecuciones de los método y donde se tarda más tiempo y con eso identificas donde tienes que empezar a depurar.
Luis Guerrero.
Nunca se me había ocurrido mirar ahí, y mira que estaba cerca 🙂
Muchas gracias.
Recomiendo este artículo para mejorar el rendimiento de JavaScript:
http://www.etnassoft.com/2012/05/17/mejorando-el-rendimiento-con-el-api-dom-desde-javascript/
El Training Center grandioso !!!
En esta página
http://mashable.com/2012/05/16/prometheus-html5-ie/
se habla de ello y que algunas de las librerias utilizadas podrán dejarse a la comunidad en GitHub.
«Beyond just creating examples of great HTML5 content, Microsoft is also focused on sharing these skills back with the community.
Gavin says that Microsoft will be publishing some of the libraries used for the Training Center on GitHub in the near future. We think that’s pretty cool.»
Saludos.