Atrasei mais uma vez esse post, mas agora é para valer, vamos fazer um passo a passo de como fazer um módulo de região para o OpenSim! Estive batalhando com módulos de região nos últimos dias e posso dizer que estou afinadíssimo para as instruções. Vou assumir algumas ferramentas (no caso Windows, .NET e Visual Studio C#), mas também desenvolvo e Linux e não é muito diferente (Linux, Mono, Monodevelop).
Vamos lá então, mão a obra!
1. OpenSim
Para começarmos a desenvolver um módulo de região, precisamos do próprio OpenSim, naturalmente. Siga as instruções da wiki, baixe o OpenSim, compile e teste. Não vou entrar nos detalhes de configuração para não fugir do foco. O importante é que todas as bibliotecas do OpenSim estejam compiladas, vamos referencia-las no próximo passo.
2. Começando um Novo Projeto
Abra o Visual Studio C# e comece uma nova solução (no meu caso chamei de OpenSimModule) e um novo projeto (usei o mesmo nome). No canto esquerdo você verá uma coluna com o nome da solução, do projeto, e dentro, uma pasta de references. Vamos precisar referenciar algumas bibliotecas. Clique o botão direito em References e escolha Add Reference. Ao abrir a janela, vá na aba Browse, siga até o local onde está sua instalação do OpenSim, entre na pasta bin, e escolha os seguintes dlls:
log4net, Mono.Addins, Nini, OpenMetaverse, OpenMetaverseTypes, OpenMetaverse.StructuredData, OpenSim.Framework, OpenSim.Region.Framework.
Adicione também as dlls de sistema System, System.Data e System.XML.
Lembrando que essas dlls são as que o seu projeto poderá referenciar com o “using”, portanto quais dlls você precisa depende de o que você quer fazer. O OpenSim.Region.Framework e OpenSim.Framework sempre serão necessários, pois eles contém os objetos da região e do OpenSim respectivamente. OpenMetaverse são as bibliotecas que o OpenSim usa para implementar o servidor do SL e também são úteis, Nini é para carregar configurações do arquivo, log4net é para usar a biblioteca Logger para imprimir dados no console e finalmente, Mono.Addins serve para encaixar o módulo no OpenSim. Cobriremos os usos no código mais adiante.
Após adicionada as referências, crie um novo item no projeto, uma classe de C# vazia, e chame do que quiser (no meu caso, chamei de MyModule).
3. Programando o Módulo
3.1 Início
Está tudo pronto para começarmos nosso módulo de região! Para começar o código, vamos incluir todas as bibliotecas que precisamos e vamos setar o Mono.Addins para que o OpenSim reconheça esta biblioteca como um módulo de região. Comece a classe desta forma:
using System; using System.Collections.Generic; using System.Reflection; using System.Text; using OpenSim.Framework; using OpenSim.Region.Framework; using OpenSim.Region.Framework.Scenes; using OpenSim.Region.Framework.Interfaces; using Mono.Addins; using OpenMetaverse; using Nini.Config; using log4net; [assembly: Addin("OpenSimModule", "0.1")] [assembly: AddinDependency("OpenSim", "0.5")] namespace OpenSimModule { [Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule")] public class MyModule : ISharedRegionModule { private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); List<Scene> m_scenes = new List<Scene>(); Dictionary<Scene, List<SceneObjectGroup>> scene_prims = new Dictionary<Scene, List<SceneObjectGroup>>(); int counter = 0; bool positive = true;
As únicas modificações que você deve fazer é o nome da classe e a primeira linha do assembly (que também será o nome do seu módulo). As outras linhas com “[” são para o Mono.Addin reconhecer que se trata de um módulo de região. A classe extende o método ISharedRegionModule, vamos falar um pouco sobre isso.
Existem dois tipos de módulos de região, um é o ISharedRegionModule e o outro é o INonSharedRegionModule. A diferença é simples: o ISharedRegionModule instancia o módulo uma única vez, e cada vez que uma nova região é adicionada ao grid, a instância é notificada. No INonSharedRegionModule, cada vez que uma região entra no grid, uma nova instância do seu módulo é criado só para atender aquela região. As diferenças ficaram claras quando entrarmos na descrição dos métodos do IRegionModuleBase.
3.2. Métodos da Interface dos Módulos:
Descrição e exemplo dos métodos que devem ser implementados para se fazer um módulo de região.
3.2.1. Initialise:
O primeiro método de um módulo de região é o Initialise. Ele é o primeiro método executado, quando o simulador ainda está carregando. Nessa parte do código a região (Scene) ainda não existe. Essa parte é geralmente utilizada para ler as configurações de arquivo usando o parâmetro passado (IConfigSource config). É possível ler qualquer opção que esteja no OpenSim.ini (veja esse manual para saber como). Como não precisamos de nenhuma configuração neste exemplo, só iremos imprimir uma mensagem que o módulo foi inicializado:
public void Initialise(IConfigSource config) { m_log.Info("[HELLOWORLD] Initializing..."); }
3.2.2. Post Initialise:
Só existe em ISharedRegionModule, é chamado apenas uma vez, após todas as regiões terem sido instanciadas. Como o INonSharedRegionModule só tem uma região, não havia necessidade de um post initialise. Não temos necessidade para esse método no nosso exemplo.
public void PostInitialise() { }
3.2.3. AddRegion:
É o método chamado cada vez que uma nova região é adicionada ao grid. Pode ser chamada várias vezes em um módulo compartilhado e apenas e um módulo não compartilhado. Aqui é o lugar para inicializar qualquer coisa que dependa da cena. No nosso caso, estamos guardando a referência da cena no nosso vetor de cenas, para podemos usar no futuro. O método recebe como parâmetro a cena que está sendo adicionada.
public void AddRegion(Scene scene) { m_scenes.Add(scene); }
3.2.4. RegionLoaded
É chamado depois que todos os métodos a serem adicionados já foram adicionados. Neste ponto, todos os outros módulos já passaram por AddRegion, portanto é o momento para criar hooks com outros módulos e se inscrever nos seus eventos. No nosso caso, aproveitamos o momento para nos inscrevermos no evento OnFrameDelegate, que conforme já explicado em outro post, é o evento da batida de coração do OpenSim. Não é muito recomendado se inscrever neste método com muitos módulos, pois esse método já tem muito o que fazer sem ter módulos extras puxando ele pra baixo, mas para efeito de exemplo, estamos nos prendendo a ele.
public void RegionLoaded(Scene scene) { scene.EventManager.OnFrame += new EventManager.OnFrameDelegate(OnTick); DoHelloWorld(scene); }
Mais para frente, veremos os detalhes do método OnTick e do método DoHelloWorld.
3.2.5. RemoveRegion
A mesma idéia do AddRegion, mas nesse caso é chamado quando a região é removida do grid. Aqui deve-se fazer as limpezas necessárias para garantir que os módulos continuem funcionando, ciente que esta região já não existe mais. No nosso exemplo, no desinscrevemos do evento e retiramos a referência da lista de cenas.
public void RemoveRegion(Scene scene) { scene.EventManager.OnFrame -= new EventManager.OnFrameDelegate(OnTick); m_scenes.Remove(scene); }
3.2.6. Close
É o método chamado quando o módulo está fechando. Usado para fazer limpezas permanentes, como apagar assets, remover referências, etc. Após este método, o módulo é fechado. No caso, não temos nada do gênero para fazer.
public void Close() { }
UPDATE:
Como foi apontado pelo Ricardo nos comentarios, ha novos metodos que precisam ser implementados do tempo que eu escrevi este tutorial. Sao eles:
3.2.7. Name:
Retorna o nome do modulo.
public string Name{ get{ return “MyHello”; } }
3.2.8. ReplaceableInterface:
public Type ReplaceableInterface{ get{ return null; } }
3.3. Métodos Extras do Módulo:
Após ver os métodos necessários para um módulo, colocaremos aqui o nosso próprio método, que será o método que irá fazer o hello world se mover pela região. Não é necessário, mas é boa política fazer um método separado para tratar a funcionalidade do módulo, e não sobrecarregar os outros métodos padrões. No nosso exemplo, temos 2 métodos, o DoHelloWorld e o OnTick. O OnTick está ligado ao evento de frames e o DoHelloWorld é o responsável por criar os objetos que formarão a palavra Hello World.
void DoHelloWorld(Scene scene) { // We're going to write HELLO with prims List<SceneObjectGroup> prims = new List<SceneObjectGroup>(); // First prim: | Vector3 pos = new Vector3(120, 128, 30); SceneObjectGroup sog = new SceneObjectGroup(UUID.Zero, pos, PrimitiveBaseShape.CreateBox()); sog.RootPart.Scale = new Vector3(0.3f, 0.3f, 2f); prims.Add(sog); // Second prim: - pos = new Vector3(120.5f, 128f, 30f); sog = new SceneObjectGroup(UUID.Zero, pos, PrimitiveBaseShape.CreateBox()); sog.RootPart.Scale = new Vector3(1, 0.3f, 0.3f); prims.Add(sog); // Third prim: | pos = new Vector3(121, 128, 30); sog = new SceneObjectGroup(UUID.Zero, pos, PrimitiveBaseShape.CreateBox()); sog.RootPart.Scale = new Vector3(0.3f, 0.3f, 2); prims.Add(sog); // Fourth prim: | pos = new Vector3(122, 128, 30); sog = new SceneObjectGroup(UUID.Zero, pos, PrimitiveBaseShape.CreateBox()); sog.RootPart.Scale = new Vector3(0.3f, 0.3f, 2); prims.Add(sog); // Fifth prim: - (up) pos = new Vector3(122.5f, 128, 31); sog = new SceneObjectGroup(UUID.Zero, pos, PrimitiveBaseShape.CreateBox()); sog.RootPart.Scale = new Vector3(1, 0.3f, 0.3f); prims.Add(sog); // Sixth prim: - (middle) pos = new Vector3(122.5f, 128, 30); sog = new SceneObjectGroup(UUID.Zero, pos, PrimitiveBaseShape.CreateBox()); sog.RootPart.Scale = new Vector3(1, 0.3f, 0.3f); prims.Add(sog); // Seventh prim: - (low) pos = new Vector3(122.5f, 128, 29); sog = new SceneObjectGroup(UUID.Zero, pos, PrimitiveBaseShape.CreateBox()); sog.RootPart.Scale = new Vector3(1, 0.3f, 0.3f); prims.Add(sog); // Eighth prim: | pos = new Vector3(124, 128, 30); sog = new SceneObjectGroup(UUID.Zero, pos, PrimitiveBaseShape.CreateBox()); sog.RootPart.Scale = new Vector3(0.3f, 0.3f, 2); prims.Add(sog); // Ninth prim: _ pos = new Vector3(124.5f, 128, 29); sog = new SceneObjectGroup(UUID.Zero, pos, PrimitiveBaseShape.CreateBox()); sog.RootPart.Scale = new Vector3(1, 0.3f, 0.3f); prims.Add(sog); // Tenth prim: | pos = new Vector3(126, 128, 30); sog = new SceneObjectGroup(UUID.Zero, pos, PrimitiveBaseShape.CreateBox()); sog.RootPart.Scale = new Vector3(0.3f, 0.3f, 2); prims.Add(sog); // Eleventh prim: _ pos = new Vector3(126.5f, 128, 29); sog = new SceneObjectGroup(UUID.Zero, pos, PrimitiveBaseShape.CreateBox()); sog.RootPart.Scale = new Vector3(1, 0.3f, 0.3f); prims.Add(sog); // Twelveth prim: O pos = new Vector3(129, 128, 30); sog = new SceneObjectGroup(UUID.Zero, pos, PrimitiveBaseShape.CreateBox()); sog.RootPart.Scale = new Vector3(2, 0.3f, 2); prims.Add(sog); // Add these to the managed objects scene_prims.Add(scene, prims); // Now place them visibly on the scene foreach (SceneObjectGroup sogr in prims) { scene.AddNewSceneObject(sogr, false); } }
Ok, agora há muito o que esclarecer, então vamos com calma, e vamos descrever em partes o que está acontecendo neste trecho.
SceneObjectGroup e SceneObjectPart
Estes são os blocos de construção do OpenSim. São os objetos do mundo. Todo objeto, também chamado de primitive, é representado internamente por um SceneObjectPart, que tem todas as informações do objeto (onde está, seu tamanho, rotação, nome, dono etc). Um SceneObjectGroup é uma coleção de um ou mais SceneObjetParts (no caso de mais de um, são vários SceneObjectParts linkados). Para um objeto aparecer em uma cena, ele precisa ser um SceneObjectGroup. Todo SceneObjectGroup tem um RootPart, que é o SceneObjectPart principal do grupo.
Vector3
Simples, é um objeto que guarda a posição, X, Y e Z genérica para uso de coordenadas cartesianas. No caso, usamos para posição do objeto.
sog.RootPart.Scale
Método pertencente ao SceneObjectPart que muda as dimensões do objeto.
PrimitiveBaseShape.CreateBox()
Cria um primitive novo, no caso uma caixa (cubo).
Pronto! Agora conseguimos entender o que este trecho de código faz. Ele inicializa diversas posições, cria cubos nessas posições e mudam suas dimensões, para fazer as partes das letras, guardando as referências na lista prims. Depois de terminar todas as letras, guarda a referência da lista de prims em um dicionário cena, lista de prims, e adiciona todos esses prims a cena usando o método AddNewSceneObject. O segundo parâmetro do método determina se esse objeto novo adicionado na cena deve ser salvo no banco de dados ou não.
Vamos analisar agora o método OnTick:
void OnTick() { if (counter++ % 50 == 0) { // Uncomment if you want to see this on your console m_log.Debug("[HELLOWORLD] Tick!"); foreach (KeyValuePair<Scene, List<SceneObjectGroup>> kvp in scene_prims) { foreach (SceneObjectGroup sog in kvp.Value) { if (positive) sog.AbsolutePosition += new Vector3(5, 5, 0); else sog.AbsolutePosition += new Vector3(-5, -5, 0); sog.ScheduleGroupForTerseUpdate(); } } positive = !positive; } }
O que este código faz é fazer a palavra hello world andar pela tela a cada frame do OpenSim. Primeiro, ele espera 50 ticks para agir. Se ele agisse em cada Tick, o processador ia disparar de tanto ter que atualizar o grupo, um tick é um espaço de tempo muito pequeno. O primeiro foreach garante que todas as cenas são iteradas, e o segundo foreach itera sob cada SceneObjectGroup, mudando a propriedade AbsolutePosition do grupo, que é um Vector3 de posição, com +5 e depois -5, em X e em Y. Desse modo, a cada frame, a palavra hello world irá se movimentar + ou – 5 (andando de um lado para o outro). Finalmente, ele faz um sog.ScheduleGroupForTerseUpdate(), que coloca esse SceneObjectGroup na fila de atualizações de posição que o simulador constantemente envia para os clientes. Dessa forma, é agilizado o processo de enviar as mudanças desse grupo para os clientes, normalmente isso é feito por um timer.
Fim do Tutorial
Demorou mas finalmente consegui escrever este tutorial! Espero que tenham gostado. Para um próximo post, prometo descrever mais coisas que podem ser feitas dentro de um módulo, para incentivar novas idéias, mas a princípio, fuçando nos objetos do OpenSim, você pode aprender o necessário para fazer o que a sua imaginação permitir.