Friday, December 02, 2016

Unity3D y Alexa trabajándo juntos

Este post fué posteado originalmente en Unity3D and Alexa working together.


Desde hace tiempo ...Tuve la idea de hacer que Unity3D y Alexa trabajen juntos...sin embargo...otros proyectos me mantuvieron ocupado y me quitaron tiempo para poder trabajar en esto...así que...hace unos días...una conversación con un amigo me hizo recordar que tenía muchas ganas de hacer esto...así que lo hice :)

Al inicio...no estaba exactamente seguro...pero lentamente las ideas se comenzaron a formar en mi mente...que pasaría si Unity leyera un webservice que es actualizado por Alexa? Cuando el comando correcto es interpretado, entonces Unity creará el objeto y problema resuelto...parece sencillo, no? Bueno...en realidad lo es...


Primero lo primero...tenemos que crear un pequeñp servidor web en NodeJS con Heroku...luego...necesitamos instalar Heroku Toolbelt...

Ahora...creamos una carpeta llamada node_alexa y dentro creamos los siguientes archivos...

package.json
{
  "dependencies": {
    "express": "4.13.3"
  },
  "engines": {
    "node": "0.12.7"
  }
}

procfile
web: node index.js
index.js
var express = require('express')
    ,app = express()
    ,last_value;

app.set('port', (process.env.PORT || 5000));

app.get('/', function (req, res) {
  if(req.query.command == ""){
 res.send("{ \"command\":\"" + last_value + "\"}");
  }else{
 if(req.query.command == "empty"){
  last_value = "";
  res.send("{}");
 }else{
  res.send("{ \"command\":\"" + req.query.command + "\"}");
  last_value = req.query.command;
 }
  }
})

app.listen(app.get('port'), function () {
  console.log("Node app is running on port', app.get('port')");
})

Una vez que tenemos esto...nos logeamos en Heroku Toolbelt y escribimos lo siguiente...

Heroku Toolbelt
cd node_alexa
git init .
git add .
git commit -m "Init"
heroku apps:create "yourappname"
git push heroku master
heroku ps:scale = web 0
heroku ps:scale = web 1

El WebService está listo para rockear -:) Deberías poder encontrárlo si vas a "http://yourappname.herokuapp.com/"

Ahora...este simple webservice potenciado por NodeJS nos servirá como un pequeño servidor de Eco...lo cual significa...cualquier cosa que escribamos nos devolverá una respuesta json...por supuesto...si escribimos "empty" entonces la respuesta será un json vacío...así que la idea principal es que podamos guardar el último valor ingresado...si pasamos un comando, este va a ser llamado cuando volvamos a llamar el servicio sin pasar ningún tipo de comando...así que si lo llamamos una vez...podemos llamarlo multiples veces sin interferir con su valor...

Lo siguiente en la lista...será crear nuestra aplicación en Unity...

Creamos una nueva aplicación y la llamamos "WebService" o algo parecido...el nombre del proyecto no importa mucho...

En la ventana de Hierarchy seleccionamos "Main Camera" y cambiamos los detalles del "Tranform" de esta forma...


Ahora, creamos un nuevo "3D Object" -> "Cube" y lo llamamos "Platform" con los siguientes detalles en el "Transform"...


Luego de eso, debemos crear cuatro paredes que van a ir alrededor de la plataforma...así que creamos 4 "3D Object" -> "Cube" y los llamamos "Wall 1", "Wall 2", "Wall 3" y "Wall 4"...





Cuando todo está listo, nuestro workspace debería verse así...


Vamos al tab project y creamos una carpeta llamada "plugins" y después creamos un nuevo archivo C# llamado "SimpleJSON"...dentro copiamos el código fuente que esta aquí...esto nos va a pemitir utlizar SimpleJSON para interpretar el JSON...

Ahora...creamos otra carpeta llamada "Script" y dentro creamos un nuevo archivo C# llamado "MetaCoding"...o cualquier otra cosa...



MetaCoding.cs
using UnityEngine;
using System.Collections;
using System.Net;
using System.IO;
using SimpleJSON;

public class MetaCoding : MonoBehaviour {

    int counter = 1;

    IEnumerator DownloadWebService()
    {
        while (true) { 
            WWW w = new WWW("http://yourapp.herokuapp.com/?command");
            yield return w;

            print("Waiting for webservice\n");

            yield return new WaitForSeconds(1f);

            print("Received webservice\n");
        
            ExtractCommand(w.text);

            print("Extracted information");

            WWW y = new WWW("http://yourapp.herokuapp.com/?command=empty");
            yield return y;

            print("Cleaned webservice");

            yield return new WaitForSeconds(5);
        }
    }

    void ExtractCommand(string json)
    {
        var jsonstring = JSON.Parse(json);
        string command = jsonstring["command"];
        print(command);
        if (command == null) { return;  }
        string[] commands_array = command.Split(" "[0]);
        if(commands_array.Length < 3)
        {
            return;
        }
        if (commands_array[0] == "create")
        {
            CreateObject(commands_array[1], commands_array[2]);
        }
    }

    void CreateObject(string color, string shape)
    {

        string name = "NewObject_" + counter;
        counter += 1;
        GameObject NewObject = new GameObject(name);

        switch (shape)
        {
            case "cube":
                NewObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
                break;
            case "sphere":
                NewObject = GameObject.CreatePrimitive(PrimitiveType.Sphere);
                break;
            case "cylinder":
                NewObject = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
                break;
            case "capsule":
                NewObject = GameObject.CreatePrimitive(PrimitiveType.Capsule);
                break;
        }
        NewObject.transform.position = new Vector3(0, 5, 0);
        NewObject.AddComponent();
        switch (color)
        {
            case "red":
                NewObject.GetComponent().material.color = Color.red;
                break;
            case "yellow":
                NewObject.GetComponent().material.color = Color.yellow;
                break;
            case "green":
                NewObject.GetComponent().material.color = Color.green;
                break;
            case "blue":
                NewObject.GetComponent().material.color = Color.blue;
                break;
            case "black":
                NewObject.GetComponent().material.color = Color.black;
                break;
            case "white":
                NewObject.GetComponent().material.color = Color.white;
                break;
        }
    }

        // Use this for initialization
    void Start () {
        print("Started webservice import...\n");

        StartCoroutine(DownloadWebService());
    }
 
 // Update is called once per frame
 void Update () {
 
 }
}

Una vez que tenemos el código...simplemente debemos adjuntarlo al Main Camera...


El concepto básico para este script es bastante simple...Estamos creándo "DownloadWebService" como un método IEnumerator para poder llamarlo como un Coroutine...y eso nos permite poder poner la función a dormir puesto que necesitamos darle un poco de tiempo entre llamadas a la función...

Este método va a llamar a nuestro WebService en Heroku buscándo un comando "create"...una vez que lo tiene...va a interpretar la respuesta JSON y dividirla en 3...para que podamos tener..."create", "blue" y "sphere"...esto va a llmar a CreateObject que luego va a crear la esfera azul...luego de que ha hecho eso...el coroutine va a continuar y simplemente va a enviar un nuevo comando a nuestro WebService para limpiar el resultado...para hacer que esto funcione de manera correcta...queremos darle un tiempo de 5 segundos después de haber el limpiado el webservice antes de poder llamar a otros comando "create"...

Y esta llamada va a ser hecha por nuestro skill en Alexa...así que basicamente cuando decimos "create blue sphere" en Alexa...ella va a enviar el comando al WebService...va a actualizar el mensaje y nuestra aplicación en Unity va a tomar el mensaje...hacer su trabajo...y luego limpiar el Webservice...luego esperar a que Alexa le provea el siguiente comando...

Así que...para ir finalizándo...necesitamos crear nuestro skill en Alexa...

Primera, vamos a crear una función Lambda...así que nos logeamos aquí...

Por supuesto...yo ya tengo todo configurado...así que vamos a crear una función dummy solamente para mostrárles los pasos...

Debemos hacer click en "Create Lambda Function" y veremos la siguiente pantalla...


Por supuesto hay bastantes...así que escribimos "Color" en el filtro...


Escogemos "alexa-skills-kit-color-expert"


Dejamos todo como está y presionamos "Next"


Escogemos un nombre y descripción...


Escogemos un rol existente si es que tenemos alguno creado anteriormente...de otra forma creamos un nuevo lambda_basic_execution...luego aumentamos el Timeout a 10 segundos y dejamos todo lo demás como está...presionamos "Next"...una ventana de confirmación va a aparecer...así que simplemente presionamos "Create function"...

Vamos a ser presentados con una ventana en donde podemos subir nuestro código fuente (lo cual vamos a hacer un poco más adelánte) y un número ARN...el cual necesitamos para un paso más adelánte...


Esta sección explica como crear el skill en Alexa...así que favor sigan mis pasos...y logeense aquí...


Escogemos "Alexa Skills Kit"...y creamos un nuevo skill...



Escogemos un nombre para nuestro skill y lo más importante...escogemos un "Invocation Name"...que es lo que le vamos a decir a Alexa para que abra nuestra aplicación..algo como..."Alexa, open Sandbox"...presionamos click...

En el tab de Interaction Model tenemos dos ventanas...llenamos esto en "Intent Schema"...

Intent Schema
{
  "intents": [
    {
      "intent": "GetUnityIntent",
      "slots": [
        {
          "name": "color",
          "type": "LITERAL"         
        },
        {
          "name": "shape",
          "type": "LITERAL"
        }
      ]
    },
    {
      "intent": "HelpIntent",
      "slots": []
    }
  ]
}

Estos son basicamente parámetros que podemos usar cuando estamos preguntándole algo a Alexa...

Y debemos llenar esto en "Sample Utterances"...

Sample Utterances
GetUnityIntent create {red|color} {sphere|shape}
GetUnityIntent create {yellow|color} {sphere|shape}
GetUnityIntent create {green|color} {sphere|shape}
GetUnityIntent create {blue|color} {sphere|shape}
GetUnityIntent create {black|color} {sphere|shape}
GetUnityIntent create {white|color} {sphere|shape}

GetUnityIntent create {red|color} {cube|shape}
GetUnityIntent create {yellow|color} {cube|shape}
GetUnityIntent create {green|color} {cube|shape}
GetUnityIntent create {blue|color} {cube|shape}
GetUnityIntent create {black|color} {cube|shape}
GetUnityIntent create {white|color} {cube|shape}

GetUnityIntent create {red|color} {cylinder|shape}
GetUnityIntent create {yellow|color} {cylinder|shape}
GetUnityIntent create {green|color} {cylinder|shape}
GetUnityIntent create {blue|color} {cylinder|shape}
GetUnityIntent create {black|color} {cylinder|shape}
GetUnityIntent create {white|color} {cylinder|shape}

GetUnityIntent create {red|color} {capsule|shape}
GetUnityIntent create {yellow|color} {capsule|shape}
GetUnityIntent create {green|color} {capsule|shape}
GetUnityIntent create {blue|color} {capsule|shape}
GetUnityIntent create {black|color} {capsule|shape}
GetUnityIntent create {white|color} {capsule|shape}

GetUnityIntent {thank you|color}

Estos son todos los comandos que Alexa puede entender...y sí...podriamos haber utilizado "Custom Slot Types" para hacer el código más corto...pero...he tenido problemas donde no funciona muy bien cuando hay mas de un slot...simplemente presionamos next...


Aquí, escogemos AWS Lambda ARN...y escogemos North America o Europe dependiendo de nuestra locación física...luego en la caja de texto...simplemente copiamos y pegamos el número ARN que recibimos de nuestra función Lambda...

Esto nos va a enviar al tab de "Test"...pero en realidad no queremos eso e inclusive no podemos usarlo...así que debemos regresar al tab "Skill Information" y encontraremos que un nuevo campo ha aparecido...

Este campo será "Application Id"...copiamos este número y continuamos hacia la parte final...

Creamos una carpeta llamada "Unity" y dentro una carpeta llamada "src"...dentro de esa carpeta copiamos el archivo "AlexaSkills.js"

Vamos a utilizar el módulo "request" de NodeJS...así que lo instalamos localmente en la carpeta Unity...

sudo npm install --prefix=~/Unity/src request 

Esto va a crear una carpeta llamada node_module conteniendo al módulo request...

Luego, creamos un nuevo archivo llamado "index.js"


index.js
var request = require("request")
  , AlexaSkill = require('./AlexaSkill')
    , APP_ID     = 'yourappid';

var error = function (err, response, body) {
    console.log('ERROR [%s]', err);
};

var getJsonFromUnity = function(color, shape, callback){

var command = "create " + color + " " + shape;

if(color == "thank you"){
 callback("thank you");
}
else{
var options = { method: 'GET',
  url: 'http://yourapp.herokuapp.com/',
  qs: { command: command },
  headers: 
   { 'postman-token': '230914f7-c478-4f13-32fd-e6593d8db4d1',
     'cache-control': 'no-cache' } };

var error_log = "";

request(options, function (error, response, body) {
 if (!error) {
  error_log = color + " " + shape;
 }else{
  error_log = "There was a mistake";
 }
  callback(error_log);
    });
}
}

var handleUnityRequest = function(intent, session, response){
  getJsonFromUnity(intent.slots.color.value,intent.slots.shape.value, function(data){
 if(data != "thank you"){
 var text = 'The ' + data + ' has been created';
 var reprompt = 'Which shape would you like?';
    response.ask(text, reprompt);
 }else{
  response.tell("You're welcome");
 }
  });
};

var Unity = function(){
  AlexaSkill.call(this, APP_ID);
};

Unity.prototype = Object.create(AlexaSkill.prototype);
Unity.prototype.constructor = Unity;

Unity.prototype.eventHandlers.onSessionStarted = function(sessionStartedRequest, session){
  console.log("onSessionStarted requestId: " + sessionStartedRequest.requestId
      + ", sessionId: " + session.sessionId);
};

Unity.prototype.eventHandlers.onLaunch = function(launchRequest, session, response){
  // This is when they launch the skill but don't specify what they want.

  var output = 'Welcome to Unity. Create any color shape by saying create and providing a color and a shape';

  var reprompt = 'Which shape would you like?';

  response.ask(output, reprompt);

  console.log("onLaunch requestId: " + launchRequest.requestId
      + ", sessionId: " + session.sessionId);
};

Unity.prototype.intentHandlers = {
  GetUnityIntent: function(intent, session, response){
    handleUnityRequest(intent, session, response);
  },

  HelpIntent: function(intent, session, response){
    var speechOutput = 'Create a new colored shape. Which shape would you like?';
    response.ask(speechOutput);
  }
};

exports.handler = function(event, context) {
    var skill = new Unity();
    skill.execute(event, context);
};

El código es bastante simple...principalmente porque la mayoría es un template...simplemente lo copia...cambias un par de cosas y estás listo para continuar...

Basicamente cuando decimos "Alexa, open Unity"...ella va a escuchar nuestros pedidos...así que podemos decir "create green cube"...y va a llamar a nuesto WebService en Heroku y luego esperar por otro comando...si no le hablas nuevamente...te va a invitar a decir algo...si le dices "Thank you" ella va a desactivarse a si misma de una manera muy educada...

Y eso es basicamente todo...una vez que Alexa envia el comando al WebServer...nuestra aplicación en Unity va a leerla y actuar como corresponde...creándo cualquier forma y color que le hemos pedido...interesánte, huh?

Pero por supuesto...no me creen, no? No puede ser tan simple...bueno...si y no...es simple...pero saqué todos los puntos complicados para poder proveerles una serie de instruciones claras y consisas...

Así que...así es como se ve cuando ejecutamos la aplicación en Unity...



Y aquí está el video en acción...


Espero que les guste...y mantenganse alertas...porque para mi esto solo fué una prueba de concepto...mi próximo proyecto va a llevar esto al siguiente nivel...

Saludos,

Blag.
Development Culture.

No comments: