openworld 0.0.3 copy "openworld: ^0.0.3" to clipboard
openworld: ^0.0.3 copied to clipboard

Openworld gaming engine using three_dart

Open World for dart #

This gaming engine is written in flutter and is based upon our existing threejs openworld repository openworldthreejs. The gaming engine uses the three_dart package making it easy to convert threejs code to three_dart. Because it uses flutter, games created with this engine are cross platform working on Android, iOS, web, Linux and windows. And with the flutter feature of hotloading it allows 3D objects to be added to a scene on the fly making game design easier.

This gaming engine includes two demo games with full source and all resources including blender models, sound, textures being freely available in this repository.

One game is set in Jerusalem at the Second Temple 72AD just before a roman invasion.

This game is not only available in this repository but also on iTunes on Amazon and web

A second game is set in Lindos, Rhodes south of Greece in 1522 just before Sulamains invasion.

image

It is also available on iTunes on Amazon and web

The philosphy of the engine it should not be bloated and only include features that typical openworld games would require on a smartphone or desktop. For example most openworld games will have animated actors, such as a person or monster walking. Most would include models, planes and sprites, sound, light, weather, time of day, maps, music and rooms. But not all would include, for example, a players inventory system or a combat system or a monetary system. So these less used features are excluded.

Alongside threedarts existing 3D functions such as loading models, lighting, texturing, shaders it also includes features that are useful in openword games such as:

Animated actors
Openworld has easy actor animation including duration, looping, cloning existing actors, assigning new textures to cloned actor, sharing animations between actors with the same skeleton, doing one acion and then transitioning to an idling action. All actors made with the Blender modelling tool and are available in '/examplesecondtemple/blender' and '/examplerhodes3d/blender'. These are then exported into the assets directory in glb format.

image

The following is sample code for adding an animated actor to a scene from an actor created in blender.

    // Load an actor in assets priests.glb using animations from another actor called 'seller'
    // Actor is from blender model:  openworld/examplerhodes3d/blender/actors/weaponer.blend
    var weaponer = await OPENWORLD.Actor.createActor(
      'assets/actors/weaponer.glb',
      shareanimations: seller,
      action:'idle',  // Actor starts with an 'idle' animation
      z: actoroffset,
    );
    weaponer.scale.set(0.0025, 0.0025, 0.0025);     // Set the scale of the actor
    OPENWORLD.Space.objTurn(weaponer, 90);          // Set angle actor is facing from north   - east
    OPENWORLD.Space.worldToLocalSurfaceObjHide(
        weaponer, -0.12, 1.3, 0, 3); // Set the position of the actor on the surface of the terrain and hide the actor when the camera is over 3 units away
    scene.add(weaponer);

Notice in '/examplerhodes3d/blender/actors/armourer.blend' it has about 20 animations while the rest of the actor have about 1 animation. This is because all the human actors in the Lindos 1522 demo share these animations. This makes it easy to add new animations without needing to add them to every actor.

It is possible to specify new textures for cloned actors and models which allows meshes to be reused and made to look different. For example for the Second Temple game a second shofar is cloned from the first and a different skin is applied:

    // Here the _shofar actor is cloned and the shofar2.jpg clone is applied to it create a second shofar that looks different
    _shofar2 = await OPENWORLD.Actor.copyActor(_shofar, randomduration: 0.1, texture:"assets/actors/shofar/shofar2.jpg");

Models
Openworld also make it easy to include models that can be cloned and also reused with different texture. Like actors, models are also made with the Blender modelling tool and are available in '/examplesecondtemple/blender' and '/examplerhodes3d/blender'. These are also exported into the assets directory in glb format with blender (except for the terrain which should be exported in wavefront objformat ).

image

    // Load a glb model from the blender model openworld/examplesecondtemple/blender/models/laver.blend
    var laver = await OPENWORLD.Model.createModel('assets/models/laver.glb',
        texture:"assets/models/temple/wood.jpg"  // Change the texture to be made of wood
    );
    OPENWORLD.Space.worldToLocalSurfaceObjHide(
        laver, 2.53,-0.51, 0.0, 7);             // Set the position of the laver on the surface and hide if over 7 units away
    laver.scale.set(0.11, 0.11, 0.11);          // Set the model scale    
    scene.add(laver);
    

Adding sprites, planes and text
Openworld also make it easy to add sprites, planes and text.

image

    // Load a sprite of gabriel 
    var gabriel = await OPENWORLD.Sprite.loadSprite(
        'assets/textures/gabriel.png', 0.5, 0.4,
        ambient: false);

    // Load a plane of coat of arms
    var coatarms = await OPENWORLD.Plane.loadPlane(
        "assets/textures/coatarms.png", 0.18, 0.2);

    // Load a plane with the text 'Lindos News'
    var textplane =   await OPENWORLD.Plane.makeTextPlane("Lindos News", Colors.black, backgroundopacity: 0);

As with actors and models, the objects can be scale and placed on the surface of terrain.

Terrain and collision detection
Openworld allows for a single terrain model to be defined. For example for the Second Temple game the terrain is defined in:

openworld/examplesecondtemple/blender/terrain/temple.blend

image

In blender you can defines multple meshes in a model as follows:

image

If the mesh contains the name 'surface' then OPENWORLD treats it as a surface and 3D objects can easily be placed onto the surface using opendarts rayscaster. The OPENWORLD function worldToLocalSurfaceObj places an object on the terrain at the point it intersects the terrain surface. Likewise a mesh with the word 'wall' in it is treated as a wall and OPENWORLD stop a player walking through it. And a 'roof' mesh is used so OPENWORLD knows if a player is indoors and stop showing rain indoors

Terrains should be exported as wavefront obj files since mesh names are retained. This is necessary so OPENWORLD knows which meshes are a wall, surface or roof

    // Example of loading terrain  openworld/examplesecondtemple/blender/terrain/temple.blend that has been exported to an obj file with path mode set to strip
    var manager = THREE.LoadingManager();
    var mtlLoader = THREE_JSM.MTLLoader(manager);
    mtlLoader.setPath('assets/models/temple/');
    var materials = await mtlLoader.loadAsync('temple.mtl');
    await materials.preload();
    var loader = THREE_JSM.OBJLoader(null);
    loader.setMaterials(materials);
    Group mesh = await loader.loadAsync('assets/models/temple/temple.obj');
    scene.add(mesh);
    // OPENWORLD uses groups in mesh to determine which meshes are surfaces, walls or roofs
    OPENWORLD.Space.init(  mesh, scene);

Spatial features
Alongside using threedarts existing spatial placement of objects OPENWORLD has extra procedures to make it easy to place objects on a terrain at a certain point and with procedures to turn and scale them. All turn angle are in degrees with 0 degrees being north and 90 being east. Spatial functions make it easy to hide 3D objects if the camera gets a certain distance away from an object helping to increase frame rate. There is also spatial lerping making it possible to move an object along a terrain from one point to another in a given amount of time. Similarly with turning an object it is possible to lerp. For example turn 90 degrees in 1 second such as in this example:

    // Example of a fountain moving south 1 unit taking 1 second to lerp and also spinning from 90 degrees to 180 degress in 1 second
    var fountain = await OPENWORLD.Model.createModel('assets/models/fountain.glb');
    OPENWORLD.Space.worldToLocalSurfaceObjHide(
        fountain, 1,1, 0.0, 7);             // Set the position of the fountain on the surface and hide if its over 7 units away
    OPENWORLD.Space.objTurn(fountain, 90);          // Set angle plane is facing east 
    scene.add(fountain);

    // Move the fountain south one unit taking 1 second to gets there - lerp the fountain
    OPENWORLD.Space.worldToLocalSurfaceObjLerp(
            fountain, 1,2, 0, 1);
   // Have fountain spin from 90 degrees to 180 degress lerping for one second
   OPENWORLD.Space.objTurnLerp(fountain, 180, 1);

There are many other spatial functions. For example:

// Place a sword 0.4 units in front of the camera
OPENWORLD.Space.placeBeforeCamera(sword, 0.4 );
// Place a sword 0.4 units in front of the camera taking 1 second to lerp to that position
OPENWORLD.Space.placeBeforeCamera(sword, 0.4, time: 1 );

// Make knight always face the camera
OPENWORLD.Space.faceObjectAlways(knight, camera);


Triggers
Openworld also has a trigger system whereby its possible to trigger an event. For example a trigger for when the camera gets a certain distance from an npc. In the following example a cat has a distance trigger that causes the cat to meow when a player moves within 4 meters of the cat:

Group cat = await OPENWORLD.Actor.createActor('assets/actors/cat.glb', z:0);
cat.scale.set(0.01,0.01,0.01);
OPENWORLD.Space.worldToLocalSurfaceObjHide(
    cat,1,1, 0, 4);
OPENWORLD.Space.objTurn(cat,0);
scene.add(cat);
OPENWORLD.BaseObject.setDistanceTrigger(cat, dist: 4);
cat.extra['trigger'].addEventListener('trigger', (THREE.Event event) {
  if (event.action) {
     OPENWORLD.Sound.play(path: "sounds/meow.mp3", volume: 0.2);
  } else {
  }
});

The is also a trigger for when a 3D object in the scene is clicked. In the following example when the minorah is clicked it is hidden:

    var minorah = await OPENWORLD.Model.createModel('assets/models/minorah.glb');
    scene.add(minorah);
    OPENWORLD.Space.worldToLocalSurfaceObjHide(minorah, -0.33, 1.03, 0.0, 4); 
    minorah.scale.set(0.11, 0.11, 0.11);
    OPENWORLD.Space.objTurn(minorah, 0);
    OPENWORLD.BaseObject.setTouchTrigger(minorah);
    minorah.extra['touchtrigger'].addEventListener('trigger', (THREE.Event event) {
       minorah.visible=false;
    });

This is also custom triggers. For example its possible to create your own trigger for when an npc is struck by a player:

  // Code when player swings sword and hits an npc triggering the struck trigger: 
  npc.extra['customtrigger'].trigger('strucktrigger', 5);

  // Code for the npc that has been struck
  OPENWORLD.BaseObject.setCustomTrigger(journalist);
  journalist.extra['customtrigger'].addEventListener('strucktrigger', (THREE.Event event) {
      if (!actorIsDead(journalist2)) {
         ...
         ...

Movement system
Openworld includes the flutter joystick widget that allows a player to control turning and movement on a smartphone.

image

This widget is used in conjuction with a threejs joystick/ keyboard that has been converted to dart. The joystick/keyboard can also be used with a keyboard allowing control on a desktop without the need for the joystick widget.

Movement also includes swiping on the screen to turn and pitch the camera.

The threedart code has been modified so that keyboard control can be switched from movement back to flutter widgets so that for example you could type text in a flutter widget.

Mob System
Openworld has procedures specifically for npcs. For example have an actor randomly walk around a point of a given distance and frequency like a dog walking around randomly:

  // Here a journalist will walk a random distance of 1.5 units from where he is at a speed of 0.2m/s, walking 10% of the time
  OPENWORLD.Mob.randomwalk(journalist, 1.5, 0.2, 0.1,
              action: "walk",     // Animation to play while walking
              stopaction: "idle", // Animation to play while idling
  );

There is also a procedure to move an NPC over and over again through a set of positions such as a guard walking up and down a corridor:

  // Here a storeman will walk through these three points walking at 0.2m/s looping over and over
  OPENWORLD.Mob.moveToLoop(
            storeman,
            [ [387.26, 285.76, 0, 0.2],
              [386.05, 281.09, 0, 0.2],
              [381.72, 276.86, 0, 0.2], ],
            action: "walk". // Animation to play while walking
       );

  // Or just move an npc through multiple points to reach a destination
  // For example have a knight walk through the point 384.76, 252.02 to reach 383.76, 252.02
   OPENWORLD.Mob.moveTo(knight, [ [ 384.76, 252.02], [ 383.76, 252.02]] ,
      action:"walk",    // Animation while walking
      stopaction:"idle" // Animation when reached final point
  );

There are also procedures to make it easy to add random chatter to an npc. For example:

  // Here the bottleshopowner will have these three sentences randomly spoken
  chatter=["I can see two of you.",
          "Sometimes I have to try out my wares",
          "Watch out for the beggar. He's a nasty piece of work"];
  OPENWORLD.Mob.setChatter(bottleshopowner, chatter);

Its also possible to have an npc say a speech with one sentence spoken after the other:

  // Here the oldlady says the following 3 sentences one after the other rather than randomly
  OPENWORLD.Mob.setSpeech(oldlady, ["I can't see very well.","Is someone there?","Try some of my pie"]);

Weather
Openworld has weather built into the engine and includes wind, rain, cloud and fog. It has lerping so it is possible gradually transition from say clear sky to cloudy or rain to fog. It also includes a random weather generator where you can specify the probabity in a given day it will rain, be cloudy, be windy or foggy.

// Set game to have moderate cloud like in the screenshot above
OPENWORLD.Weather.setCloud(0.5);

Time
Time of day can be specified using a skymat shader that allows for specifying the azimuth so can generate sunsets, sunrises, noon, night.

image

image

Also the day length can be specified. For example 1 hour to equal 24 hours. Therefore a full day can occur in one hour. Openworld can change ambience so at night the terrain will be darker than during the day. Time has been incorporated into the weather system so for example if its foggy at night the fog is black while during the day the fog will be grey:

image

Light
Openworld includes features like specifying that a light should be switched on at night and turned off during the day. Also allows light to flicker and also to lerp turning a light on so its not sudden.

    var homelight= new THREE.PointLight(0xFFA500);
    homelight.intensity = 1.0; 
    homelight.distance = 1.2;
    // Specify that homelight will flicker
    OPENWORLD.Light.addFlicker(homelight);
    // Specify that homelight will only be on at night
    OPENWORLD.Light.addNightOnly(homelight);

Sound
Openworld has a sound system including a pool of flutter audioplayers which can be reused when a sound has finished playing. The pool also allows all audioplayers to be silenced when the game is closed and reactivated when the game is restarted. The sound system also includes features like volume, looping, fading and also delay.

// Play the sound of rain
OPENWORLD.Sound.play( path: 'sounds/rain.mp3',
   fadeIn:1,  // Take 1 second to gradually fade the sound of rain in to volume 0.1
   volume: 0.1);

Object Selection
Openworld allows for an object to be highlighted. When combined with a touch trigger this allows for an item to be highlighted further when clicked.

image

For example clicking on the minorah in the Second Temple and read information about it:

image

This code shows an example of how its possible to highlight an object and combine it with a touch trigger to do something like give information when the player clicks the highlighted object:

    // Create a minorah in the second temple and then highlight it with setHighLight making it bluish
    // letting the player know they can click it. Then add a touch trigger to when click on the minorah
    // give the player information about it
    var minorah = await OPENWORLD.Model.createModel('assets/models/minorah.glb');
    scene.add(minorah);
    OPENWORLD.Space.worldToLocalSurfaceObjHide(minorah, -0.33, 1.03, 0.0, 4); 
    minorah.scale.set(0.11, 0.11, 0.11);
    // Highlight the minorah to show that can click it
    OPENWORLD.BaseObject.setHighlight(minorah, scene, THREE.Color(0x0000ff), 0.25);
    OPENWORLD.BaseObject.highlight(minorah, true,scale:1.05, opacity:0.15);
    OPENWORLD.BaseObject.setTouchTrigger(minorah);
    minorah.extra['touchtrigger'].addEventListener('trigger', (THREE.Event event) {
      // Display information about the minorah when click it
      OPENWORLD.BaseObject.highlight(minorah, true, scale:1.05, opacity:0.25);
     
      var clickevent=event.action;
      setState(() {
        menuposx=clickevent.clientX;
        menuposy=clickevent.clientY-40;
        menuitems.clear();
        menuitems.add({"text":"The menorah is made of pure gold."});
        ...
        ...
      });
    });

Delays
There are many openworld procedures that have a delay. For example it is possible to have an actor wave his hand in 5 seconds or have cat meow in 5 seconds

  // Cat meows in 5 seconds
  OPENWORLD.Sound.play(path: "sounds/meow.mp3", volume: 0.2, delay:5);

The allows for a sequence of actions to be performed. For example have a knight spin in 1 second, jump in 2 seconds and then laugh in 3 seconds.

// Knight turns east in 1 seconds
OPENWORLD.Space.objTurnLerp(knight, 90, 0.5, delay:1);
// Knight jumps in 2 seconds
OPENWORLD.Actor.playActionThen( knight, "jump", "idle", delay: 2);
// Knight laughs in 3 seconds
OPENWORLD.Sound.play(path: "sounds/laugh.mp3", volume: 0.2, delay:3);

Persistence
Openworld has persistence so data can be stored when an app is closed and all game information can be retrieved. For example remember the state of the weather:

   // Store the rain level
   OPENWORLD.Persistence.set("rain", 1.0);

   // When restart the game set the rain back to what it was when last saved
   Weather.setRain( await OPENWORLD.Persistence.get("rain",def:0));

Rooms
Openworld has a room system which allows things to happen when a player enters a room such as play a background sound, determine if the room is indoors and call a trigger when a player enters the room. Rooms are defined to have a central x,y point with a rectangle having distance from that point. Rooms not only have a looping sound associated with it but also a random intermittent sound such as a smithy hammering occasionly. If a room is indoors then rain will not appear. The following is an example of a room with a background sound and a random intermittent sound and is defined to be indoors if a roof is above the players head. When the player enters the room a priest says a speech:

    var roomBC = OPENWORLD.Room.createRoom(7.4, 1.26,               // central coordinates of the room
             soundpath: "sounds/courtyard.mp3", volume: 0.05,       // looping background sound played when enter the room
             randomsoundpath:"sounds/prayer.mp3", randomsoundgap:50 // prayer is played randomly every 50 seconds
    );
    scene.add(roomBC);
    OPENWORLD.Room.setAutoIndoors(roomBC, true);                    // If roof is above head is 
    OPENWORLD.Room.setDistanceTrigger(roomBC,
        minx: 5, maxx: 9.1, miny: -0.67, maxy: 2.8);                // define the boundaries of the room - room in x axis from 5 to 9.1 and y axis is -0.67 to 2.0
    // When enter the room
    roomBC.extra['trigger'].addEventListener('trigger', (THREE.Event event) {
      if (event.action) {
         OPENWORLD.Mob.setSpeech(priest, ["Welcome to the Second Temple","Its 72AD"]);   // When enter the room the priest says a speech
      } 
    });

Maps
Openworld has maps that show the players position with a marker. It is possible to have multiple maps where you can have a map for say a city and then a larger encompassing for wilderness. Each map is an image and you specify two points with a pixel position and a corresponding world position. With the two points openworld calculates the image position from the world position through interpolation and can place a marker in the correct position:

In this example there are two maps and when the player is within the area of map.jpg then this map is shown. If is outside this area then maplarge.jpg is shown.

    var maps = [
      OPENWORLD.MapItem('assets/maps/maplarge.jpg',
          worldx: -7.61,  // Point 1 with pixel position and corresponding world position
          worldy: 17.73,
          imagex: 301,
          imagey: 338,
          worldx2: 11.05,  // Point 2 with pixel position and corresponding world position
          worldy2: -12.99,
          imagex2: 371,
          imagey2: 450),
      OPENWORLD.MapItem('assets/maps/map.jpg',
          worldx: -2.36, // Point 1 with pixel position and corresponding world position
          worldy: 4.94,
          imagex: 13,
          imagey: 122,
          worldx2: 9.52, // Point 2 with pixel position and corresponding world position
          worldy2: -2.32,
          imagex2: 977,
          imagey2: 588)
    ];
    OPENWORLD.Maps.init(maps);

    // Given the current world position get the correct map and its marker position on that map
    var map = OPENWORLD.Maps.getMapFromWorldcoords(worldpos.x, worldpos.y);

Music
Openworld can play music in the background with multiple songs with different probabilities of playing. In the example below there are four song and hatikvah has a 5/8 chance of the song being chosen to play. By default there is a gap between songs of 3 minutes plus a random number between 0 and 50 second though this can be changed with 'timebetweensongs' and 'randomtimebetweensongs':

    var musics = [
      OPENWORLD.MusicItem('sounds/hatikvah.mp3', chance: 0.5),
      OPENWORLD.MusicItem('sounds/harp.mp3', chance: 0.1),
      OPENWORLD.MusicItem('sounds/harp2.mp3', chance: 0.1),
      OPENWORLD.MusicItem('sounds/harp3.mp3', chance: 0.1),
    ];
    OPENWORLD.Musics.init(musics);
    OPENWORLD.Musics.timebetweensongs=6*60;

    // Randomness on how long before play a song
    OPENWORLD.Musics.randomtimebetweensongs=60;

Config file
Rather than hard code all 3D objects in flutter it is possible to specify 3D objects in a configuration file specified in assets as 'config.json'. For example to define vegetation such as grass in many locations a grass object can be defined and then cloned in any number of positions. In the example below a grass model is loaded called 'grass' and three grass models are placed on the surface at different positions ("p") and different scales ("s") and different turn ("t"). All will disappear if the player is more than 4 units away ("d")

{"objects": [
  {"name": "grass","object": [
    {"type": "model", "filename":"assets/models/grass.glb","s":0.03}
  ]},
  ],

"positions":
{"staticpositions":[
      {"name": "grass", "p": [412.20,307.97,0], "d":4},
      {"name": "grass", "p": [412.50,307.27,0], "s":0.5, "d":4},
      {"name": "grass", "p": [410.50,306.27,0], "s":0.6, "t":90, "d":4}
]
}]
  }
}

The config file also has pool objects. This is useful for example with a forest with thousands of trees. Its possible to show only the closest 40 trees at anyone on time instead of clone 1000 trees. In the example below a tree object is created. There are two pool positions at two different positions with 500 trees defined in each that are randomly placed within 30 units of the position of the pool object. They are randomly scaled by 0.4 (scaled randomly between 0.8 to 1.2). So the example below defines 1000 trees but only creates 40 and displays only the closest 40 to the player.

{"objects": [
  {"name": "tree","object": [
    {"type": "model", "filename":"assets/models/tree.glb","s":1.5}
  ]},
  ],

"positions":
{  "poolpositions":[{"poolsize":40,  "positions":[
    { "name": "tree","p": [  -20.84, 20.39,0.0],  "rx": 30, "ry":30, "rs":0.4, "n": 500 },
    { "name": "tree","p": [  -42.05, 11.56,0.0],  "rx": 30, "ry":30, "rs":0.4, "n": 500 },
  ]
}]
  }
}

Common objects
Openworld includes some useful objects like fire, water, flares, smoke and sky. Most are based on those available in threejs as shaders.

image

The following is an example of fire:

    fire = new VolumetricFire(
        2,4,2,0.5, camera);
    await fire.init();

    OPENWORLD.Space.worldToLocalSurfaceObjHide(fire.mesh,3.29, 0.25,0.3,3);
    scene.add(fire.mesh);
    OPENWORLD.Updateables.add(fire);

The following is an example of flares:

image

The following is an example of water:

image

Multi player
The demo games include simple multi player capability. But multiplayer is not built into the openworld gaming engine. This might change in the future. Openworld does have a client class with sessions and player information but the actual game play is specific for each demo. The demo multiplayer simply broadcasts a logged in players position and turn and broadcasting a player action 'wave'. The multiplayer does provide chat and information on whos on. The server side is written in php and mysql as available in the repository in /server

image

Hotloading
The demo games include a hotloading function that is called whenever a game is hotloaded. This allows everything in hotload function being reloaded and displayed instantly making placement of objects being possible on the fly. For example in the example below an extra grassposs position could be added and then hotloaded and it will be displayed straight away:

hotload() async
{

    _hotload.clear();
    // All the grass
    const grassposs = [
      [13.61, 1.54, 0.0, 4],
      [14.41, 1.82, 0.0, 4],
      [16.67, 1.33, 0.0, 4],
      [14.60, 5.79, 0.0, 4],
      [59.54, -0.12, 0, 4],
      [58.10, 0.35, 0, 4]
    ];
    var grass = await OPENWORLD.Sprite.loadSprite(
        'assets/textures/grass.png', 0.1, 0.1);
    for (var tree in grassposs) {
      var cyprusii = await OPENWORLD.Sprite.cloneSprite(grass);
      OPENWORLD.Space.worldToLocalSurfaceObj(cyprusii, tree[0].toDouble(),
          tree[1].toDouble(), tree[2].toDouble());
      _hotload.add(cyprusii);
    }
}

An easy way to get a world position on the terrain is from the android studio console which will every second display your location and turn:

pos: 8.59, 1.31, t-90.00
pos: 8.59, 1.41, t-90.00
pos: 8.59, 1.51, t-95.00

By copying and pasting the position into the hotload function, objects can be placed in the scene, hotloaded and viewed straight away.