Getting Started with Region Modules
From OpenSimulator
Hello World
This brief tutorial is intended to get people started with developing applications with/for opensim using region modules and the opensim API. This approach to developing virtual world applications, unique to OpenSim, is a powerful alternative to the well-known inworld scripting approach. The very simple module available here writes "HELLO" with prims on every region of your opensim instance, and makes them move every 2 seconds or so. Please note:
- The opensim API is rapidly changing. The code available in this example has been written and tested for opensim release 0.6.0, which corresponds to SVN 7176. The example has also been tested for a more recent stable version, SVN 7320. You are encouraged to use 7176, if possible.
- You are also encouraged to try this code on the default standalone region first before running on a grid.
- The resulting dll will run both in Windows and Linux, but the building environment is assumed to be VC# on Windows. Users of other operating systems will be able to follow, though.
To get started:
- Download and install opensim (preferably 7176), and build it as normal. Run it once, so you create a default region and a default user. Then shut it down.
- Get this zip file: http://www.ics.uci.edu/~lopes/opensim/HelloWorld-current.zip. Unzip it somewhere.
- Before you go changing the code of the application, see its effect inworld by doing this:
- Grab HelloWorld/bin/Release/HelloWorld.dll and dump it in opensim/bin
- Start opensim as normal, and login to it.
You should see the word HELLO spelled out in prims, and moving every so often.
Are you ready to explore the code now?
Hold on. Before we do that, let me give you the 30-second introduction to Visual C#. Visual C# is similar to Eclipse, if you ever used that. When it builds, it places the resulting dll or exe in whatever folder you tell it to (right-click on the project->properties). By default it places them in bin/Release. When you compile it with debugging, that goes into bin/Debug. In spirit, the dll is equivalent to a jar file. That's what you want to produce and pass around. The PDB file can be ignored, unless you want to debug. The HelloWorld example uses the defaults of VC#, so every time you build it without debugging, the dll is placed under HelloWorld/bin/Release. To build without debugging information, simply right-click on the solution in the Solution Explorer window and choose Build.
Another note: the solution file included in the zip is for VC# 2005. If you have VC# 2008 that's fine too. Just double-click on the solution file, and VC# 2008 will convert the whole thing.
And now for the linux/mac/unix folk: We now have a nant default.build file included in the tutorial zip - just drop it in the same directory with the source. Note that for everything to go well, you will need to park your module source directory inside opensim/bin/. When you are ready to build, open a shell, change to the module source directory, and then run 'nant' without any args.
OK, now we're ready. Go ahead and double-click HelloWorld.sln.
The only class in this example is called HelloWorldModule. I know you're eager to get to the part where objects are created and moved around, but I'm afraid that's the easy part. In order to be able to write those functions effectively, you will need to understand a lot more of the engineering of these modules, and that's the part that's not so trivial, especially if you aren't familiar with VC#. So let me go through the code very slowly.
using System; using System.Collections.Generic; using System.Reflection; using log4net; using Nini.Config; using OpenMetaverse; using OpenSim.Framework; using OpenSim.Region.Environment; using OpenSim.Region.Environment.Interfaces; using OpenSim.Region.Environment.Scenes;
This first part consists of a collection of "using" declarations. If you come from Java, those declarations are equivalent to "import" declarations. And you know what that means in Java: the jar files must be reachable for compilation to succeed. Same here: the dlls for those elements must be reachable for this project to build. Luckily for you, I have included those dlls in the zip file, so you don't need to add them. They are all happily bundled in HelloWorld/bin/Release. Go ahead and look there. However, as you start getting cozy with this code and you start wanting more, more, more, you will need more from the OpenSim API and even from other libraries. When that comes, you will need to add more dlls to your project. That is done through VC#, not on the file system directly! To add libraries ("References"), right-click on "References" in the Solution Explorer, and choose "Add Reference". A small window will pop up with a few tabs. If the library you need is under the System namespace, you want to interact with the .NET tab. If the library you need is from OpenSim you need to interact with the "Browse" tab; then, navigate to your installation of opensim bin, and pick the dlls you need.
namespace HelloWorld { public class HelloWorldModule : IRegionModule {
This second part is the namespace and class declaration. The only noteworthy thing here is the ": IRegionModule" part. What that means is that our class HelloWorldModule implements the IRegionModule interface, which means that we have to implement the 5 methods of that interface, namely: Initialise (yes, it's the British spelling...), PostInitialise, Close, Name, and IsSharedModule. But that's not all. OpenSim treats IRegionModule classes in a very special way. When OpenSim starts, it looks into all the dlls it can reach (under its bin) in search for classes that implement the IRegionModule interface. All of those classes are then acquired and run by OpenSim as if they were part of OpenSim.
And this is the key to OpenSim application development: you can add your own code, or somebody else's code, as a plug-in that runs natively on the server. Yeepie!
private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
This third block above is the declaration of the m_log variable. This variable helps us output messages into a log. I'm just using a common idiom that is used all over OpenSim and that uses log4net. If you come from Java, I'm sure this rings a bell... if you've never seen this, you don't need to understand it, just use it to output messages both onto the console and onto the OpenSim.log file.
List<Scene> m_scenes = new List<Scene>(); Dictionary<Scene, List<SceneObjectGroup>> scene_prims = new Dictionary<Scene, List<SceneObjectGroup>>(); int counter = 0; bool positive = true;
The fourth block of code is the declaration of a few instance variables for our HelloWorld example, and this is where we start getting acquainted with the OpenSim API.
- m_scenes will hold references to all the scenes. "What's a scene?", I hear you asking. A scene is OpenSim's representation of the contents of a region. If your opensim has only one region, there will be only one scene object; if it has more, there will be as many.
- scene_prims is a dictionary that associates scenes with a list of objects of type SceneObjectGroup. In the Java world this would probably be a Hashtable; in the .NET world it's a Dictionary. scene_prims will hold references to the prims that we will instantiate for constructing the world HELLO in each scene. An important note: if you look inside OpenSim's Scene class, you will see that it has a list of entities corresponding to all objects and avatars present in the scene. As we place prims in scenes, including these HELLO prims, they will be placed on that list. However, what we are doing here is constructing a set of prims that are special for our application and that we want to track throughout our application; they are our managed prims. As such, we want to hold references to those, and only those. That's what scene_prims is for.
- The other two variables, counter and positive, are explained later.
#region IRegionModule interface public void Initialise(Scene scene, IConfigSource config) { m_log.Info("[HELLOWORLD] Initializing..."); m_scenes.Add(scene); } public void PostInitialise() { m_scenes[0].EventManager.OnFrame += new EventManager.OnFrameDelegate(OnTick); foreach (Scene s in m_scenes) DoHelloWorld(s); } public void Close() { } public string Name { get { return "Hello World Module"; } } public bool IsSharedModule { get { return true; } } #endregion
The fifth block is the implementation of the IRegionModule methods. There's nothing much here, and this is how it should be: encapsulate all of your code in your own functions. Let's go through the most important of these methods:
- Initialise (I still can't get over this British spelling): this method is called by OpenSim when it is discovering all the IRegionModule classes in dlls. OpenSim sends us two parameters: a scene object and a configuration object. The configuration object allows us to browse through the OpenSim configuration, if we need that info. The scene object is the very important scene information that we want to hold on to; the scene object is our code's main link to OpenSim, it's how we get to access and modify things in the world and beyond. So the main thing our Initialise method does is to hold on to the scene object that OpenSim sends us.
- PostInitialise (argh, British spelling again): this method is called by OpenSim once everything is ready to go, that is, after OpenSim has properly set all the internal things it needs to set. You should look at PostInitialise as the Main method of our application modules. In this case, we are doing two things:
- We are subscribing to an OpenSim event called OnFrame. This event is the Heartbeat of the simulator, so if our module is to do things periodically, we may want to tag along this heartbeat. We are going to move the word HELLO periodically, that's why we're subscribing to this event. Note that this is not always the best approach, though. The simulator is a busy bee, and has to do lots of things already without our module being there. Tagging along the simulator's heartbeat means that we're making it do more stuff. If you're planning to develop modules with your own simulations, you're much better off defining your own timer object, because that will make the tick function run on a different thread than the simulator's heartbeat. But in this case, this is just HelloWorld, so we can abuse gently.
- For each scene, we are constructing the HELLO word.
- Close and Name are trivial -- see the documentation.
- IsSharedModule is very important, and may be a source of much grief. Here's the deal: OpenSim can either (a) instantiate our HelloWorldModule class exactly once, a singleton, independent of the number of regions; or (b) instantiate our HelloWorldModule class as many times as the number of regions, creating a different HelloWorldModule instance for every scene. These two modes are captured by the IsSharedModule method. If you want OpenSim to create a singleton, make this method return true ("yes, OpenSim, my module is shared"); if you want to create different instances for different regions, make this method return false ("no, OpenSim, my module is not shared among the regions"). Depending on your application, you may want one thing or the other. In this case, I want to centralize the management of my specially constructed prims, so I made it be shared. (In reality for this simple example, it wouldn't matter). But here's the important idiom: when you have a shared module, you want to have a list of scenes, just like the m_scenes variable in this example; when you have a non-shared module, you want to have only one scene variable declared in your module, so that would be "Scene m_scene;" instead of "List<Scene> m_scenes;"
Alright! If you read up to here, and followed the story, you're now ready to move on to the fun part.
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); ... // 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); } }
This method creates 12 SceneObjectGroup (SOG) instances corresponding to the 12 segments that constitute the word HELLO. BIG WARNING: SOG is currently under heavy redesign, and may be replaced with another class altogether in future versions of opensim. But for 0.6.0 SOG is tha man. SOG represents a linked group of objects, the linksets in SL, with root part and all. If you have your VC# open with HelloWorld, go to this method, and type, somewhere: sog. -- just like that, stopping at the dot. VC# will show you everything that is inside SOG instances. And that is A LOT! It's one of those monster classes! (and that's the reason why it's being redesigned) You should spend some time exploring what's inside SOG. In this example, I'm only using a couple of things: one of the constructors and the RootPart. For the RootPart, I'm only setting its Scale, i.e. the size of the prim.
At the end of this method, when all prims are instantiated, they're added to the list of managed prims (scene_prims) and they're finally made visible on the scene, by calling scene.AddNewSceneObject. The second argument to this method, false, means that we're telling OpenSim not to back these objects up on the DB; we want them to be non-persistent.
And finally we come to the end of the application:
void OnTick() { if (counter++ % 50 == 0) { 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; } }
This is the worker method in our application for every heartbeat of the simulator. Notice that I have a counter there so that things only move every 50 heartbeats. I also have a direction (positive) that can be true or false, so that the prims move back and forth. As you can see, the way to move prims is to set their AbsolutePosition property directly. After that, we tell opensim to schedule the object for an update, so that we can see it move. Note this last bit, the scheduling for updates, is still a fragile piece of the opensim API; we may need it here or not, depending on the version of opensim you're using.
Voila!
Running on custom configurations (grids, alternate terrain) should work fine if you take into account the following:
- Make sure that the Z value in the C# code is above the terrain. Instead of 30,you may wish to raise or lower this value. Make sure to also modify 29 and 31, similarly.
- Change UUID.Zero to your a UUID for an avatar that has build rights on your region. You can replace "UUID.Zero" with "new UUID("your UUID string here")"
- This C# module will place "HELLO" on each region in your grid. If you want to make it so that "HELLO" appears in only one region, try this quick replacement:
foreach (Scene s in m_scenes) if (s.RegionInfo.RegionName == "YourRegionName") DoHelloWorld(s);
What Next
This is an example of 100 continuously moving blocks.
Here are some other suggestions for what you can do next, on your own:
- Change the Color of the prims
- Complete the example by creating prims for the word WORLD. For the W you probably need our beloved quaternions...
- Add more regions to your opensim and see what happens
- Change this module from shared to non-shared and see what happens (nothing much different, one hopes, in this simple case)
- Be more creative with the movement and make the prims move in circles
- ...
Remember, every time you make changes to the application code, and you build it, its dll is placed under bin/Release, so you have to copy it to opensim/bin. If you want, you can change the build settings of your application, so that it places the dll directly in opensim/bin.
Have Fun!
Tips
There is very little documentation about the OpenSim API. One of the reasons for that is that it is still evolving and therefore unstable. A reasonable place to look at the API is http://docs.opensimulator.org/namespaceOpenSim.html; in there, the most interesting place to start is the Region.Environment.Scenes namespace http://docs.opensimulator.org/namespaceOpenSim_1_1Region_1_1Environment_1_1Scenes.html.
The code itself is full of information, if you can cope with looking at too much information. Here's what I do and recommend: when you're developing these modules, have 2 VC# open, one with your application and the other with opensim. My experience with opensim is that the names of things are quite reasonable. So whenever you want something that you don't know exactly how to get, go to the opensim code and make heavy use of the Search functions, especially Edit->Find and Replace->Find in Files. VC#, just like Eclipse, also has those wonderful navigation facilities that take you to definitions from calls, etc. Just right-click on things in the code.