using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using AtomicEngine; using OpenSimplex; public class Terrain { private readonly Scene _scene; private readonly Material _surfaceMaterial; private readonly Material _chunkMaterial; private readonly DecorationLibrary _decorationLib; private readonly List _chunks = new List(); // Generator configuration private const int Chunksize = 20; private const int Chunkheight = -100; private int chunksToGenerate = 200; private const float NoiseScaleX = .05f; private const float NoiseScaleY = 6; private const int SurfaceRepeatPerChunk = 6; private const float SurfaceSegmentSize = 0.5f; private const int PercentageOfChunksWithCrates = 4; // Generation working variables private OpenSimplexNoise _noise = new OpenSimplexNoise(); private Random _rng = new Random(); private Vector3 _lastSurfaceExtrusion = Vector3.Right*Chunksize; private int _discard = 0; private class DecorationLibrary { private T[][] _groups; private Random _rng = new Random(); public DecorationLibrary(params T[][] spritesGroups) { _groups = spritesGroups; } public T GetRandomFromGroup(int group) { if (group >= _groups.Length) throw new Exception($"There's no group with index {group} in this library"); return _groups[group][_rng.Next(_groups[group].Length)]; } public List GetAllResources() { List allList = new List(); foreach (T[] group in _groups) { allList = allList.Concat(group).ToList(); } return allList; } } public Terrain(Scene scene) { _scene = scene; // We load the materials, the ones in this example only use built in techniques, we use diffuse for the ground for performance _surfaceMaterial = Cache.Get("materials/UnlitAlpha.xml"); _surfaceMaterial.SetTexture(0, Cache.Get("scenarios/grasslands/surface.png")); _chunkMaterial = Cache.Get("materials/Unlit.xml"); _chunkMaterial.SetTexture(0,Cache.Get("scenarios/grasslands/ground.png")); // We create and populate a library with the decorations Func GetSprite = file => Cache.Get($"scenarios/grasslands/Object/{file}"); _decorationLib = new DecorationLibrary( new [] {GetSprite("Bush (1).png"), GetSprite("Bush (2).png"), GetSprite("Bush (3).png"), GetSprite("Bush (4).png")}, new [] {GetSprite("Mushroom_1.png"), GetSprite("Mushroom_2.png"), GetSprite("Stone.png"), GetSprite("Sign_2.png")}, new [] {GetSprite("Tree_1.png"), GetSprite("Tree_2.png"), GetSprite("Tree_3.png")} ); // We setup the hotstop/origin of each sprite to be next to its bottom foreach (Sprite2D sprite in _decorationLib.GetAllResources()) { sprite.SetHotSpot(new Vector2(0.5f, 0.1f)); } // We generate the chunks and add some boxes randomly Sprite2D crateSprite = Cache.Get("scenarios/grasslands/Object/Crate.png"); for (int i = 0; i < Chunksize*chunksToGenerate; i+=Chunksize) { GenerateChunk(i); // Crates if (_rng.Next(100) < PercentageOfChunksWithCrates) { Node crateNode = AtomicMain.CreateSpriteNode(crateSprite, 3); crateNode.SetPosition(new Vector3(i + _rng.Next(8), 20, -5)); CollisionBox2D crateCollider = crateNode.CreateComponent(); crateCollider.SetSize(0.76f, 0.76f); crateCollider.SetDensity(1.0f); crateCollider.SetRestitution(0.6f); crateCollider.SetFriction(0.4f); } } } public float SampleSurface(float posX) { return (float)_noise.Evaluate(posX*NoiseScaleX, 0)*NoiseScaleY; } void GenerateChunk(int startx) { // We create a node and position where the chunk starts Node node = _scene.CreateChild(); node.SetPosition2D(startx, 0); // We create components to render the geometries of the surface and the ground var groundComponent = node.CreateComponent(); groundComponent.SetMaterial(_chunkMaterial); groundComponent.BeginGeometry(0, PrimitiveType.TRIANGLE_LIST); var surfaceComponent = node.CreateComponent(); surfaceComponent.SetMaterial(_surfaceMaterial); surfaceComponent.BeginGeometry(0, PrimitiveType.TRIANGLE_LIST); // We initialize and add a single entry to the surface collider points list List surfacePoints = new List() {new Vector2(0, (float)_noise.Evaluate(startx*NoiseScaleX, 0)*NoiseScaleY ) }; // We translate the last surface extrusion point so it's local relative to the chunk we're creating _lastSurfaceExtrusion += Vector3.Left*Chunksize; // We loop all the segments in this chunk float incr = SurfaceSegmentSize; for (float x = 0; x < Chunksize-float.Epsilon*8; x+=incr) { // We store vars for the position for the current segment end point x position and for the y position of the 4 points float xEnd = x+incr; float tlY = SampleSurface(startx + x); float trY = SampleSurface(startx + xEnd); float blY = tlY + Chunkheight; float brY = trY + Chunkheight; // We create vectors that represent Vector3 bl = new Vector3(x, blY, -10); Vector3 tl = new Vector3(x, tlY, -10); Vector3 br = new Vector3(xEnd, brY, -10); Vector3 tr = new Vector3(xEnd, trY, -10); // We add the top right point to the surface points list (remember we added the first point in the list init) surfacePoints.Add(new Vector2(tr)); // We call the CreateDecor function passing the global position of the current segment end point and the segment angle CreateDecor(tr+Vector3.Right*startx, tl-tr); // We create the geometry of the surface (UV os oriented according to the surface) Vector2 startV = Vector2.UnitX*(x/Chunksize)*SurfaceRepeatPerChunk; Vector2 endV = Vector2.UnitX*(xEnd/Chunksize*SurfaceRepeatPerChunk); //bl surfaceComponent.DefineVertex(_lastSurfaceExtrusion); surfaceComponent.DefineTexCoord(startV); //tl surfaceComponent.DefineVertex(tl); surfaceComponent.DefineTexCoord(startV-Vector2.UnitY); //tr surfaceComponent.DefineVertex(tr); surfaceComponent.DefineTexCoord(-Vector2.UnitY+endV); //bl surfaceComponent.DefineVertex(_lastSurfaceExtrusion); surfaceComponent.DefineTexCoord(startV); //tr surfaceComponent.DefineVertex(tr); surfaceComponent.DefineTexCoord(-Vector2.UnitY+endV); //br - We store the last point to use for continuity _lastSurfaceExtrusion = tr + Quaternion.FromAxisAngle(Vector3.Back, 90)*Vector3.NormalizeFast(tr - tl); surfaceComponent.DefineVertex(_lastSurfaceExtrusion); surfaceComponent.DefineTexCoord(endV); // We create the geometry of the ground (UV is in world coordinates) //bl groundComponent.DefineVertex(bl); groundComponent.DefineTexCoord(new Vector2(bl/Chunksize)); //tl groundComponent.DefineVertex(tl); groundComponent.DefineTexCoord(new Vector2(tl/Chunksize)); //tr groundComponent.DefineVertex(tr); groundComponent.DefineTexCoord(new Vector2(tr/Chunksize)); //bl groundComponent.DefineVertex(bl); groundComponent.DefineTexCoord(new Vector2(bl/Chunksize)); //tr groundComponent.DefineVertex(tr); groundComponent.DefineTexCoord(new Vector2(tr/Chunksize)); //br groundComponent.DefineVertex(br); groundComponent.DefineTexCoord(new Vector2(br/Chunksize)); } // We commit the geometry data we just created surfaceComponent.Commit(); groundComponent.Commit(); // We create the collider component for the chunk surface CollisionChain2D surfaceCollider = node.CreateComponent(); surfaceCollider.SetLoop(false); surfaceCollider.SetFriction(1); surfaceCollider.SetVertexCount((uint) surfacePoints.Count+1); _chunks.Add(surfaceCollider); // We add a small overlapping segment with a bit of negative offset in y so the wheel passes smoothly over chunk seams Vector2 smoother = new Vector2(-incr*.5f, (float) _noise.Evaluate((startx-incr*.5f)*NoiseScaleX, 0)*NoiseScaleY-.005f); surfaceCollider.SetVertex(0,smoother); // Finally, we set the vertex of the surface collider for (int c = 0; c < surfacePoints.Count; c++) { Vector2 surfacePoint = surfacePoints[c]; surfaceCollider.SetVertex((uint)c+1, surfacePoint); } // A collider must have a rigid body to interact with other bodies, even if static which is the case node.CreateComponent().SetBodyType(BodyType2D.BT_STATIC); } void CreateDecor(Vector3 position, Vector3 leftVector) { // We discard a few calls _discard++; if (_discard % 3 != 0) return; // We use the simplex noise to evaluate chances with smooth transition double chance = _noise.Evaluate(position.X * 0.2f, 0)+1; Node node = null; bool isTree = false; // Common decorations (grasses) if (chance < 1.2f) { // We discard a few of these too if (_rng.Next(6) < 4) node = AtomicMain.CreateSpriteNode(_decorationLib.GetRandomFromGroup(0), 3, false); } else if (chance < 1.3f) node = AtomicMain.CreateSpriteNode(_decorationLib.GetRandomFromGroup(1), 4, false); else if (chance < 1.7f) { // We only spawn trees if the surface isn't too steep if (Math.Abs(leftVector.Y) < 0.1f) { node = AtomicMain.CreateSpriteNode(_decorationLib.GetRandomFromGroup(2), 4, false); node.Scale2D = node.Scale2D *= 1 + _rng.Next(30)/100f; isTree = true; } } if (node != null) { // We position the node on the surface, if the node is a tree we don't rotate it AND offset it a bit node.SetPosition(position+Vector3.Forward*15); if (!isTree) { Quaternion transformation = Quaternion.FromRotationTo(Vector3.Left, leftVector); node.SetRotation(transformation); } else node.Translate(-node.Up*0.1f); } } }