Jump to content
  • Advertisement


  • Content Count

  • Joined

  • Last visited

Community Reputation

20 Neutral

1 Follower

About Lyfe

  • Rank

Personal Information

  • Role
    Game Designer
  • Interests


  • Twitter
  • Github

Recent Profile Visitors

1871 profile views
  1. It has been quite some time since we posted something. And it's true, for the most part we didn't really do much. Work on Lyfe just wasn't such a high priority as university for example. But we're back. And with that we switched engines. To be fair, there wasn't much work we had to replicate. Most of the time we invested so far was learning how to handle simple things in Unreal when we all had way more experience in working with Unity. A mistake. But I think we're on the right path now. This of course means there is no real gameplay footage to show. The actual gameplay is simply less of a priority compared to the editor so we're focusing on that and trying to get a stable version running. For more information on the concept behind the cell editor or at least the basic sculpting check out this entry. I won't go into detail about it here. Today I will focus on the technical side of implementing metaballs and having the calculation run on your GPU rather than your CPU. I'm doing this partly to show we're actually working again but als since it took me way too long to figure out how this works and I didn't have good resources. So feel free to use our code and adapt it to your needs. Anybody here who doesn't know how metaballs work or what they even are? Here are some links for you if you're interested: metaballs (what even are they?) metaballs in blender (a bit more detailed) maths behind metaballs Let's start with the basics: The basic shape of the cell is formed by nodes that are connected to each other. Each of these nodes is the center/anchor for a metaball. This means each of these nodes exudes a charge in the field around it. This field is simply represented by points in 3d space. If two nodes are close to each other the charges sum up. This results in smooth transitions between blobs/metaballs. For more details click on the third link or write a comment. On to the C# script of our editor. This is where we prepare our data and read the chargefield once it's done computing. private float[] CalculateCharges(Vector3Int _resolution) { float[] charges = new float[_resolution.z * _resolution.y * _resolution.x]; List<SNode> nodes = baseNode.GetAllChildNodeMetaData(new List<SNode>()); int kernel = cs_Charges.FindKernel("CSMain"); ComputeBuffer nodeBuffer = new ComputeBuffer(nodes.Count, SNODE_SIZE); nodeBuffer.SetData(nodes.ToArray()); ComputeBuffer chargeBuffer = new ComputeBuffer(charges.Length, sizeof(float)); chargeBuffer.SetData(charges); cs_Charges.SetBuffer(kernel, "nodes", nodeBuffer); cs_Charges.SetBuffer(kernel, "charges", chargeBuffer); cs_Charges.SetVector("EDITOR_GRID_DIMENSION", new Vector4(_resolution.x, _resolution.y, _resolution.z, Meta_CellEditor.SCULPTING.GRID.SCALE)); cs_Charges.SetVector("basePos", transform.position); cs_Charges.SetInt("numNodes", nodes.Count); cs_Charges.Dispatch(kernel, _resolution.x / 8, _resolution.y / 8, _resolution.z / 8); chargeBuffer.GetData(charges); chargeBuffer.Dispose(); nodeBuffer.Dispose(); return charges; } So. Since the basis for metaballs is chargefield this method takes a Vector3 for the resolution of this field. We then make a float array of that size to give to our compute shader and have it filled there. List<SNode> nodes = baseNode.GetAllChildNodeMetaData(new List<SNode>()); This line simply collects all child nodes and stores their metadata in a simple struct that can be transferred to the shader. public struct SNode { public Vector3 position; public Vector3 distortion; public float cubePortion; public float radius; } This is how the struct looks. The information in cludes the position within the charge field with 0/0/0 being the center. The distortion/deformation of the sphere so if it's more of an oval. The cube portion, so if it's more of a sphere or a cube (not fully implemented yet) and the radius. Pretty simple stuff. int kernel = cs_Charges.FindKernel("CSMain"); Next we find our kernel within our shader so we can pass values to it and run it. The actually initialized this shader in an Init method beforehand. cs_Charges = Resources.Load<ComputeShader>("Shaders/cs_Charges"); ComputeBuffer nodeBuffer = new ComputeBuffer(nodes.Count, SNODE_SIZE); nodeBuffer.SetData(nodes.ToArray()); For this next part I can only recommend using ComputeBuffers. Just initialize it with the count of elements and the size of each element. In our case this is the size of 8 floats. After that we push our data into the buffer. ComputeBuffer chargeBuffer = new ComputeBuffer(charges.Length, sizeof(float)); chargeBuffer.SetData(charges); We do exactly the same thing for our charges. And this is where buffers come in handy: You can not only use them to get data into your shader but also to read it from there if you write it into a buffer. cs_Charges.SetBuffer(kernel, "nodes", nodeBuffer); cs_Charges.SetBuffer(kernel, "charges", chargeBuffer); cs_Charges.SetVector("EDITOR_GRID_DIMENSION", new Vector4(_resolution.x, _resolution.y, _resolution.z, Meta_CellEditor.SCULPTING.GRID.SCALE)); cs_Charges.SetVector("basePos", transform.position); cs_Charges.SetInt("numNodes", nodes.Count); Next we set the buffers in our shader. It's important that every one of these strings corresponds to a variable of the same type in your shader. If you only want to transfer a single value, you don't need to use the big ComputeBuffers. The simple methods SetVector/SetInt/SetFloat will do. This is actually where the naming of your variables is shown to not be the best: resolution isn't actually the relsolution but rather the dimension and together with the scale makes up the resolution. cs_Charges.Dispatch(kernel, _resolution.x / 8, _resolution.y / 8, _resolution.z / 8); And this is where we actually run our shader code. I'll behonest here and tell you that I'm not quite sure how these last 3 parameters work but so much: each one stands for the number of core groups. So basically we're using 8 groups of 8 groups of 8. So we divide what we need to calculate to split it evenly on all these cores. This becomes really important if you want the parallelization to work. chargeBuffer.GetData(charges); chargeBuffer.Dispose(); nodeBuffer.Dispose(); return charges; At the end we simply read the data our shader stored in the buffer and dispose our buffers to free the memory. Anybody familiar with C or C++ knows this is to prevent memory leaks. That is all the C# code basically. Simple stuff and no maths. It only get's worse from here on. // Each #kernel tells which function to compile; you can have many kernels #pragma kernel CSMain struct Node { float3 position; float3 distortion; float cubePortion; float radius; }; StructuredBuffer<Node> nodes; //node positions in world space int numNodes; float4 EDITOR_GRID_DIMENSION; //xyz are dimension, w is scale float3 basePos; //position of the base node RWStructuredBuffer<float> charges; float3 GridPosToLocalPos(float3 _gridPos) { _gridPos -= EDITOR_GRID_DIMENSION.xyz / 2.f; _gridPos *= EDITOR_GRID_DIMENSION.w; return _gridPos + basePos; } float CalculateCubeCharge(Node _node, float3 _voxelPos) { return 0; } float CalculateSphereCharge(Node _node, float3 _voxelPos) { float3 d; d.x = pow(_voxelPos.x - _node.position.x, 2); d.y = pow(_voxelPos.y - _node.position.y, 2); d.z = pow(_voxelPos.z - _node.position.z, 2); float r_squared = (d.x * _node.distortion.x + d.y * _node.distortion.y * 12.f + d.z * _node.distortion.z) / pow(_node.radius * 12, 2); if (r_squared < 0.5f) { return 1.f - r_squared + pow(r_squared, 2); } else { return 0; } } float CalculateCharge(Node _node, float3 _voxelPos) { float r = (_node.cubePortion * CalculateCubeCharge(_node, _voxelPos)) + ((1.f - _node.cubePortion) * CalculateSphereCharge(_node, _voxelPos)); return r; } [numthreads(8,8,8)] void CSMain (uint3 id : SV_DispatchThreadID) { float3 voxel = id; float3 voxelPos = GridPosToLocalPos(voxel); for (int n = 0; n < numNodes; n++) { int index = id.z * (EDITOR_GRID_DIMENSION.x * EDITOR_GRID_DIMENSION.y) + id.y * EDITOR_GRID_DIMENSION.x + id.x; charges[index] += CalculateCharge(nodes[n], voxelPos); } } So. This is the entirety of our shader code for the metaballs. We still need to tweak some of the static values so please, feel free to experiment when using this code. The struct at the top is basically our SNode. After that our buffers and set values follow. Note that nodes is just a StructuredBuffer while charges is a RWStructuredBuffer. The RW means read-write. So the shader can not only read the values stored but also write in there. I think the best way to go through this is from top to bottom. float3 GridPosToLocalPos(float3 _gridPos) { _gridPos -= EDITOR_GRID_DIMENSION.xyz / 2.f; _gridPos *= EDITOR_GRID_DIMENSION.w; return _gridPos + basePos; } This method simply turns a gridposition to a position in local space. Easy as that. Calculate cube charges is not implemented yet. (sorryyyyyy... We'll add how that works once we got it figured out.) float CalculateSphereCharge(Node _node, float3 _voxelPos) { float3 d; d.x = pow(_voxelPos.x - _node.position.x, 2); d.y = pow(_voxelPos.y - _node.position.y, 2); d.z = pow(_voxelPos.z - _node.position.z, 2); float r_squared = (d.x * _node.distortion.x + d.y * _node.distortion.y + d.z * _node.distortion.z) / pow(_node.radius, 2); if (r_squared < 0.5f) { return 1.f - r_squared + pow(r_squared, 2); } else { return 0; } } And here we simply calculate the charge one node exudes on one point in space. I myself am not sure about all the maths in there but again, you can read up on that by clicking on the third link at the begininng of this. float CalculateCharge(Node _node, float3 _voxelPos) { float r = (_node.cubePortion * CalculateCubeCharge(_node, _voxelPos)) + ((1.f - _node.cubePortion) * CalculateSphereCharge(_node, _voxelPos)); return r; } This function again calculates the charge on one point from one node but factors in the cube and sphere portion of the corresponding node. [numthreads(8,8,8)] void CSMain (uint3 id : SV_DispatchThreadID) { float3 voxel = id; float3 voxelPos = GridPosToLocalPos(voxel); for (int n = 0; n < numNodes; n++) { int index = id.z * (EDITOR_GRID_DIMENSION.x * EDITOR_GRID_DIMENSION.y) + id.y * EDITOR_GRID_DIMENSION.x + id.x; charges[index] += CalculateCharge(nodes[n], voxelPos); } } And finally we come to the actual core method. And here we find our 8/8/8 again. These values need to correspond to the ones when dispatching the shader. Otherwise you won't be able to use the id correctly. In your case the id points to the point in space we want to calculate the charge for. We use our GridPosToLocalPos here. This is important or all our beautiful metaball would be anywhere but where we want them to be. Below that we iterate over all nodes and sum up their influences on this one point in space. For a 200 * 200 * 10 chargefield with 10 nodes in it this code runs in less than a second. And that's why we use compute shaders, children. But for real, this would not be feasable when running on a CPU. But I'm 100% sure this code could be improved even further. Anyway, I hope this code helps someone who wanted to use compute shaders but didn't know how or wanted to implement metaballs. Cheers and keep on evolving.
  2. Hello once again to this entry to our devblog. This one will show the basics how we reworked the compound clouds to look - let's be honest - a lot better than they did before. The last version consisted of one class CompoundCloud which was represented by a procedural mesh component. That one started out as a simple cube and then offset the positions of the vertices by a randomly chosen amount between two previously chosen values. That looked... functional at best. And interaction with the player was pretty much not practical and computationally expensive. So the next idea we came up with was to make a cloud of sphere meshes and just have the player collect every one of those. This resulted in a new hierarchy. At the top there's still the CompoundCloud but it now has an array of Compound_ParticleComponents for its representation. Each one of those has a sphere mesh to handle collision and a particle system to look pretty. A short demo of the new system can be found here. We should probably start with the particle system. Not many steps requiered here. 1. Create a new particle system 2. Click on "Required" and in the first section ("Emitter") set a material (will be shown later) and the Screen Alignment to "PSA Rectangle" (this might not make a difference to the standard but it's set to that for our system). Then scroll down and set a cutout testure in the section "Particle Cutout". We used our heart icon you might have seen in one of our update videos. (we also changed the background color to something light so you can actually see what is happening) 3. Click an Spawn. There's a little more to do here. First of all set the rate in the "Spawn" section to "Distribution Float Uniform". Same for the rate scale. Play around with the values a bit or just take what we got there. Next set the particle burst method to interplated. The burst scale distribution should be "Distribution Float Particle Parameter". Once again play around with the values until you have something you like. 4. Go into "Lifetime" and set Min to 2 and Max to 1.1 (Again, once you're done with this tutorial just play around with this values) 5. Go to "Initial Velocity" and reduce the Min and Max values. We recommend between 2 for all Max and -2 for all Min. This will keep your particles closer together and not have them shoot in one direction. 6. Next up add a seeded initial location and set its bounds to something you like. 7. And the final step is to add a seeded sphere. You should now have something roughly looking like this. The Material: (absolutly nothing special) Now for the interesing part: The code. This is perfectly doable in blueprints, too but since we're mostly working in C++ I'll show how we solved it in code. I'm also only gonna show the constructors of the CompoundCloud class and the Compound_ParticleComponent class since this tutorial mostly deals with the look of the clouds. If you're interested in how any other part works just let me know and maybe I'll make a short explanation for that in the future. The code then: uint8 particleCount = StaticMaths::RR(CLOUD_PARTICLE_MIN, CLOUD_PARTICLE_MAX); We get a random value for the number particle systems we want to use. In our case this is our own function that simply determines a number between two other numbers. std::string center = "CenterSystem"; UCompound_ParticleComponent_Cell* temp = CreateDefaultSubobject<UCompound_ParticleComponent_Cell>(FName(center.c_str())); particles.Add(temp); RootComponent = temp; Then we set up the center component. All the other systems will circle around this one. This also functions as the rootComponent for the actor. The UCompound_ParticleComponent_Cell is the second class and we will deal with it later. for (uint8 i = 0; i < 100; i++) { for (int j = 0; j < pow(i, 2); j++) { //define the name for the current particle system //ParticleSystem_<circleNum>_<Num>_<randomSeed> std::string name = "ParticleSystem_"; name.append({ static_cast<char>((i + 1)) }); name.append({ "_" }); name.append({ static_cast<char>(j + 1) }); name.append({ "_" }); name.append({ static_cast<char>(j + i) }); //create the particle system with the newly defined name UCompound_ParticleComponent_Cell* temp = CreateDefaultSubobject<UCompound_ParticleComponent_Cell>(name.c_str()); particles.Add(temp); temp->SetupAttachment(RootComponent); //set up random location within a circle double a = (((double)rand() / (RAND_MAX)) + 1) * 2 * PI; double r = (CLOUD_RADIUS_STEPS * (i + 1)) * sqrt(((double)rand() / (RAND_MAX)) + 1); double x = r * cos(a); double y = r * sin(a); FVector location = FVector(x, y, 0); temp->SetRelativeLocation(location); //finally: check if number of elements in array is particle count //if so, stop this loop if (particleCount - 1 == particles.Num()) { break; i = 100; j = pow(i, 2); } } if (particleCount - 1 == particles.Num()) { break; i = 100; } } This part is where the magic happens. Basically we want to have somewhat circular shapes around the center system. So the outer for-loop with i counts the circles. The 100 is a dummy value since there will never be that many circles and it would be a waste of resources to actually calculate the true number of circles. We only need to know the number of particleComponents which is our particleCount. The inner loop with j counts from 0 to i to the power of 2. So on every circle there are i*i particleComponents. Next up is a bit of naming. Not really relevant. Then we create another particleComponent and add it to the actor and the array. What comes next might be interesting for some: this formular basically determines a random position on a circle. So we take (i + 1) times our pre-defined cloud radius steps to get the radius of our current circle and we have all the data we need. Everything else can be determined from that and a random number. Whe then set that location for the particleComponent. At the end of the inner loop we check if we already have all the particles we need. If so set i and j to their max values so the loops stop. This is why we didn't need to calculate how many circles there will be when we start the loops. Don't worry, the particleComponent involves less maths. //instantiate mesh component and particle system component particleSystem = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("ParticleSystem")); mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh")); particleSystem->SetupAttachment(this); mesh->SetupAttachment(this); //Collision binding mesh->bGenerateOverlapEvents = true; mesh->OnComponentBeginOverlap.AddDynamic(this, &UCompound_ParticleComponent_Cell::BeginOverlap); mesh->OnComponentEndOverlap.AddDynamic(this, &UCompound_ParticleComponent_Cell::EndOverlap); We create default subobject for the mesh and the particles system and then bind the collision functions. Easy as that. //get the mesh and set it auto meshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Game/Meshes/ball.ball'")); if (meshAsset.Object != nullptr) { mesh->SetStaticMesh(meshAsset.Object); mesh->SetVisibility(false); mesh->RelativeScale3D = FVector(5.f); } else { Logging::Log("Could not find Asset 'ball' at path in Compound_ParticleComponent_Cell"); } Then we load the mesh and set it. We also scales up the sphere mesh since it turned out to be way too small and the player would miss it a lot of the time. (That Logging::Log there doesn't really concern you. It's a function we wrote that simple writes a message into a file and onto the screen. Helpful for debugging. I left it in there for this tutorial because I think you should always have something in your code tell you when something goes wrong.) //get the needed particle system and set it in the component try { //static ConstructorHelpers::FObjectFinder<UParticleSystem> psAsset(TEXT("ParticleSystem'/Game/ParticleSystems/PS_CompoundCloud_SingleCelled.PS_CompoundCloud_SingleCelled'")); auto psAsset = ConstructorHelpers::FObjectFinderOptional<UParticleSystem>(TEXT("ParticleSystem'/Game/ParticleSystems/PS_CompoundCloud.PS_CompoundCloud'")); if (psAsset.Succeeded()) { particleSystemType = psAsset.Get(); particleSystem->SetTemplate(particleSystemType); } else { Logging::Log("Could not find Asset 'PS_CompoundCloud_SingleCelled' at path in Compound_ParticleComponent_Cell"); } //particleSystem->Template = particleSystemType; } catch (int e) { Logging::Log("Could not find Asset 'PS_CompoundCloud_SingleCelled' at path in Compound_ParticleComponent_Cell\nCause: FObjectFinder Access Violation"); Logging::Log(e); } We had some trouble loading particleSystems this way so I left in both ways to do it and the try-catch block in case one of you might have a similar problem. So that's basically it. Bye and keep on evolving.
  3. Lyfe is set out to be a spiritual successor to the now relatively old game Spore by Maxis. This is a project born from love for the idea of a game about evolution. So far our team consists of only three people but we plan on expanding this. Technicals: A rough DesignDocument.pdf is already in the works and done to a point where we can start to work on at least the first two stages. We plan on realizing this project in an already existing game engine since we are a small team of people working on this in their spare time and don't have the capacity to write our own. Currently this would be Unreal Engine 4.
  4. And so we meet once again to discuss what's new in the development of Lyfe. But as we promised on our Twitter this is something big. I don't have to tell you what we're discussing, you probably already read the title of this entry. I have to thank Jake Best who took the time draw the concept we developed for how the player will create the basic shapes for his cells. So let me explain what is going on here. You start off with just one central orb. This is your base node and your cell starts to 'grow' from it. There are arrows on four sides of this base node. If you pull on them new nodes will be created in that direction. This will create a kind of 'spine' for your cell. As shown in fig. 3 and 4 not only is the default setting when creating new nodes to the left or the right to make your cell symmetrical but you can also move your nodes/ bend the spines and the shape will be translated to the other side as well. You can also scale single nodes (fig. 5/6). This expands the cytoplasm around this node. The next thing is something we decided we needed but are not sure how to implement: a weight/thickness slider. This includes the overall amount of cytoplasm with a slight bias for filling up space between spines. Next step: placing organelles. Most organelles like Mitochondria or a Golgi Apparatus can only be placed on the inside of your cells but all organelles that are used for interaction with the outside world like Flagella or Pseudopodia have to be placed on the membrane. Organelles will have options to deform them, like elongating. Rotating them around the anchor point on the cell is also an option. Asymmetry is an option that not only affects the placement of the organelles but also the sculpting of your cell. In fig. 11 the right half of the cell is deleted resulting in this more potato shaped creation. Below are some possible results you might achieve with this toolset. For the two on the right, the dashed line represents their shape if the weight-slider was put to maximum and the one on the far left is a more experimental idea with actually two split nodes. If this will be possible in future is not sure yet, but we will see. That's it for this entry. Feel free to comment all your questions and suggestions. Keep on evolving! cellcreator.pdf
  5. Once again we have a new entry to this devblog. First of all some logistics: We will post new updates every other week from now on because we just don't make enough breakthroughs to justify wasting your time every week. And we decided to categorize these entries. So far we got "Update" and "Feature". Updates will deal with the current state of the game while Features are more theoretical and will explain certain features we are planning on implementing or are currently implementing. So let's jump right in, shall we? The cell stage is coming along nicely. You can see the current state in this video on our ... Could that be? On our brand new YouTube channel! There are still two minor bugs with the compound clouds: #1 Vertices only move until they are in the center then stop there; Solution: Move the cloud end reorient the vertices as soon as one hits the center #2 You can basically swim through the cloud and as long as you don't get too close to any vertex it won't deform; Solution: If you are between a vertex and the center move that verteg towards the center of the mesh One feature we wan't to add that isn't in the code yet: Calculate the value of the clouds (the amound of the compound you can extract from it) based on the mesh's volume. So far it's a static value and the mesh simply disappears once it's zero. Also you don't gain any compounds from consuming the clouds yet. But that will change within the next week. During that time we will also implement the different types of clouds. So far there is no solution in sight on how we will generate the cloud representation so we can finally make the mesh invisible. This is our main hurdle at the moment. There is now a Logging function that writes important messages into a log file, e.g. if the connection to the database failed. That's right: We now got a database connected to Lyfe. There will be a separate DevBlog on its structure once it's fully planned out. We decided on SQLite. Mostly because it does everything we need and is easy to use. Another thing that's currently being discussed is how we should save your creations. The first idea was to use XML: <Creature name="" stage="creature"> <Backbone count="6"> <!-- starting at 0 then move in one direction and note all vertebrae then the other direction alignment in the degree varying from horizontal --> <vertebra id="0" parent="" alignment=""/> <vertebra id="1" parent="0" alignment=""/> <vertebra id="2" parent="1" alignment=""/> <vertebra id="3" parent="0" alignment=""/> <vertebra id="4" parent="3" alignment=""/> <vertebra id="5" parent="4" alignment=""/> </Backbone> <Limbs count="2"> <!-- type are the prefabs you pull into the creator x and y position on parent body mesh or something Limb set as tail/one central leg etc are symmetrical but not split --> <limb id="0" parenttype="body" type="" positionX="" positionY="" symmetry="y" split="y"/> <limb id="1" parenttype="body" type="" positionX="" positionY="" symmetry="y" split="n"/> </Limbs> <Extremities count="2"> <!-- Extremities can also be put onto the last vertebra--> <extremity id="0" parenttype="limb" parent="0" type="" scale=""/> <extremity id="1" parenttype="limb" parent="1" type="" scale=""/> <extremity id="2" parenttype="vert" parent="5" type="" scale=""/> </Extremities> <!-- ... ... ... ... ... --> </Creature> But one of the problems was finding a proper way to parse it while directly constructing your creation after each parsed node. Also XML files are gerenally constructed in scripts like C# or JS (from my experience; correct me if I'm wrong). This would create some additional steps while storing your creation so we decided against it. The next thing is a quite simple text file that looks pretty much like a config file and our programmer came up with: #CREATURE creature #BACKBONE 6 #VERTEBRA 0 -1 0 #VERTEBRA 1 0 0 #VERTEBRA 2 1 0 #VERTEBRA 3 0 0 #VERTEBRA 4 3 0 #VERTEBRA 5 4 0 #LIMBS 2 #LIMB 0 body -1 -1 -1 y y #LIMB 1 body -1 -1 -1 y n #EXTREMITIES 2 #EXTREMITY 0 limb 0 -1 1 #EXTREMITY 1 limb 1 -1 1 #EXTREMITY 2 vert 5 -1 1 As you can see the structure is preserved so all parent objects are listed before their children so the creature can be built line by line. All the stats are missing a defining name now and this is the only let down with kind of structure: it's not that easy on the eye and probably hard to understand. But it's easy to write, easy to parse and a lot smaller. The second one only is a third the size of the first one (after you remove the comments). I guess that's it for this week. And as always we would appreciate if you shared your thoughts with us. Keep on evolving!
  6. Lyfe

    Functionality - Cell Stage

    Of course you can. These yellow blobs are the compound clouds or rather the colliders for the compound clouds so the player can interact with them.
  7. We're back with another update. First a little recap of what happened this week: We got our own banner and logo. Thanks go out logomakr.com where we made it. Our website is now in develpoment. I got no idea when it will be online but we're working on it. We started using Taiga.io to manage the development. It was frustraing to just have a good a idea or realised something that needed fixing/reworking in the concept and just forget about it or have it in an unorganised text file. We got a new team member: Helices, who will function as a Biology Advisor to this project. On that note: We are reworking the cell stage to be more realistic (as far as it doesn't sabotage fun gameplay in any way). We will do another entry on that once we deem it presentable. On the actual development front: The procedural meshes now work mostly as intended. They are being generated randomly from a basic cube shape with 26 vertices. For now that should work and adding more is basically just busywork if we really need more than that. When the player collides with this mesh all vertices with a distance to the player that's within a certain threshold will move away from the player based on a vector from the player center to the vertex. This functionality is planned out so far but not implemented. Slicing the mesh will be the next, more complicated step. Next thing I will be working on besides the website: The procedural environment. Since it would be pretty silly to just but up barriers around the map it should be infinite. To make this feasible all content has to be procedurally generated around the player. Logically it will be deleted again as soon as the player has a certain distance to it. This is true for all passive, floating objects, compound clouds and other cells. The distance should be high enough to feel natural for there to be change but also not so high that it affects the framerate. The rework on the cell stage results in there being five new compounds replacing the previous ones: CO2, Oxygen, Amino Acids, Glucose and Lipids. The will definitely be differentiated by their color and maybe by the shape of the clouds if it is deemed useful for the player and doable for a programmer. Interaction with other cells will be an interesting part. I don't want to unveil anything that will be part of the cell stage rework but I'll tell you everything we have for certain: To absorb other cells you simply have to move over them. If you have 30% more mass than they do - which is calculated via your organelles and your cytoplasm - you will absorb them and vice versa. One thing we want to get sort of experimental with is sound. Despite the background music there will be no ingame sound effects. And even the background music will mostly consist of some atmospheric sounds. So far there was no time to prototype this but we will try to get to it soon but for now planning and coding has a higher priority. That's it for this week's update. I'll leave you with a little insight into our code with which we generate the compound cloud mesh. void ACompoundCloud_Cell::CreateCloudMesh() { //vertex buffer //TArray<FVector> vertices; //Front vertices.Add(FVector(50.f, -50.f, -50.f));//0 vertices.Add(FVector(50.f, 0.f, -50.f)); vertices.Add(FVector(50.f, 50.f, -50.f)); vertices.Add(FVector(StaticMaths::RR(50.f, 150.f), StaticMaths::RR(-150.f,-50.f), 0.f)); vertices.Add(FVector(StaticMaths::RR(100.f, 200.f), 0.f, 0.f)); vertices.Add(FVector(StaticMaths::RR(50.f, 150.f), StaticMaths::RR(50.f, 150.f), 0.f));//5 vertices.Add(FVector(50.f, -50.f, 50.f)); vertices.Add(FVector(50.f, 0.f, 50.f)); vertices.Add(FVector(50.f, 50.f, 50.f)); //Left //2 vertices.Add(FVector(0.f, 50.f, -50.f)); vertices.Add(FVector(-50.f, 50.f, -50.f)); //10 //5 vertices.Add(FVector(0.f, StaticMaths::RR(100.f, 200.f), 0.f)); vertices.Add(FVector(StaticMaths::RR(-150.f, -50.f), StaticMaths::RR(50.f, 150.f), 0.f)); //8 vertices.Add(FVector(0.f, 50.f, 50.f)); vertices.Add(FVector(-50.f, 50.f, 50.f)); //Back //10 vertices.Add(FVector(-50.f, 0.f, -50.f)); //15 vertices.Add(FVector(-50.f, -50.f, -50.f)); //12 vertices.Add(FVector(StaticMaths::RR(-200.f, -100.f), 0.f, 0.f)); vertices.Add(FVector(StaticMaths::RR(-150.f, -50.f), StaticMaths::RR(-150.f, -50.f), 0.f)); //14 vertices.Add(FVector(-50.f, 0.f, 50.f)); vertices.Add(FVector(-50.f, -50.f, 50.f));//20 //Left //16 vertices.Add(FVector(0.f, -50.f, -50.f)); //0 //18 vertices.Add(FVector(0.f, StaticMaths::RR(-200.f, -100.f), 0.f)); //3 //20 vertices.Add(FVector(0.f, -50.f, 50.f)); //6 //Bottom //16 //15 //10 //21 vertices.Add(FVector(0.f, 0.f, -50.f)); //9 //0 //1 //2 //Top //6 //7 //8 //23 vertices.Add(FVector(0.f, 0.f, 50.f)); //25 //13 //20 //19 //14 //index buffer //+++++ Front //Lower Left indices.Add(3); indices.Add(1); indices.Add(0); indices.Add(1); indices.Add(3); indices.Add(4); //Lower Right indices.Add(4); indices.Add(2); indices.Add(1); indices.Add(2); indices.Add(4); indices.Add(5); //Upper Left indices.Add(6); indices.Add(4); indices.Add(3); indices.Add(4); indices.Add(6); indices.Add(7); //Upper Right indices.Add(7); indices.Add(5); indices.Add(4); indices.Add(5); indices.Add(7); indices.Add(8); //+++++ Right //Lower Left indices.Add(5); indices.Add(9); indices.Add(2); indices.Add(9); indices.Add(5); indices.Add(11); //Lower Right indices.Add(11); indices.Add(10); indices.Add(9); indices.Add(10); indices.Add(11); indices.Add(12); //Upper Left indices.Add(8); indices.Add(11); indices.Add(5); indices.Add(11); indices.Add(8); indices.Add(13); //Upper Right indices.Add(13); indices.Add(12); indices.Add(11); indices.Add(12); indices.Add(13); indices.Add(14); //+++++ Back //Lower Left indices.Add(12); indices.Add(15); indices.Add(10); indices.Add(15); indices.Add(12); indices.Add(17); //LowerRight indices.Add(17); indices.Add(16); indices.Add(15); indices.Add(16); indices.Add(17); indices.Add(18); //Upper Left indices.Add(14); indices.Add(17); indices.Add(12); indices.Add(17); indices.Add(14); indices.Add(19); //Upper Right indices.Add(19); indices.Add(18); indices.Add(17); indices.Add(18); indices.Add(19); indices.Add(20); //+++++ Left //Lower Left indices.Add(18); indices.Add(21); indices.Add(16); indices.Add(21); indices.Add(18); indices.Add(22); //Lower Right indices.Add(22); indices.Add(0); indices.Add(21); indices.Add(0); indices.Add(22); indices.Add(3); //Upper Left indices.Add(20); indices.Add(22); indices.Add(18); indices.Add(22); indices.Add(20); indices.Add(23); //Upper Right indices.Add(23); indices.Add(3); indices.Add(22); indices.Add(3); indices.Add(23); indices.Add(6); //+++++ Bottom //Lower Left indices.Add(21); indices.Add(15); indices.Add(16); indices.Add(15); indices.Add(21); indices.Add(24); //Lower Right indices.Add(24); indices.Add(10); indices.Add(15); indices.Add(10); indices.Add(24); indices.Add(9); //Upper Left indices.Add(0); indices.Add(24); indices.Add(21); indices.Add(24); indices.Add(0); indices.Add(1); //Upper Right indices.Add(1); indices.Add(9); indices.Add(24); indices.Add(9); indices.Add(1); indices.Add(2); //+++++ Top //Lower Left indices.Add(23); indices.Add(7); indices.Add(6); indices.Add(7); indices.Add(23); indices.Add(25); //Lower Right indices.Add(25); indices.Add(8); indices.Add(7); indices.Add(8); indices.Add(25); indices.Add(13); //Upper Left indices.Add(20); indices.Add(25); indices.Add(23); indices.Add(25); indices.Add(20); indices.Add(19); //Upper Right indices.Add(19); indices.Add(13); indices.Add(25); indices.Add(13); indices.Add(19); indices.Add(14); TArray<FVector> normals; for (int i = 0; i < vertices.Num(); i++) { normals.Add(vertices[i] / vertices[i].Size()); } TArray<FVector2D> uv0; TArray<FProcMeshTangent> tangents; ////The colors applied to every vertex and blended on the surfaces TArray<FLinearColor> vertexColors; mesh->CreateMeshSection_LinearColor(0, vertices, indices, normals, uv0, vertexColors, tangents, true); //Enable collision data mesh->ContainsPhysicsTriMeshData(true); mesh->bUseComplexAsSimpleCollision = false; mesh->SetSimulatePhysics(true); } If you made it to this part you probably read the code and in that case: We are still looking for anyone who wants to contribute to this journy into the unknown. And please don't look at me like that, the code is functional if not beautiful. Thanks, bye.
  8. This week I want to give you some technical insight into our project. I want to start with recapping what happened over the last week: The player movement for the cell stage is implemented and the GUI is wired up, too. There is now a health-bar, DNA-bar and you can see your current compounds as well as your compound balance. Currently I'm working with or rather exoerimenting with Unreal's UProceduralMeshComponent. My main goal is to create a mesh I can manipulate on runtime. The first thing I want to use it for are the compound clouds. When the player swims into them they should bend around him as if they were being absorbed while simoultaniously decreasing their value until it reaches 0 and the dissovle completly. Once I'm familiar with how this class works I will use it for the creature editor. So far it's the best thing I know of for creating meshes that can be generated on runtime. Currently I'm struggeling with writing an algorithm that creates a mesh that roughly outlines the shape for the compound cloud and mostly acts as a trigger and the bounds for the simulated clouds. On that point I discovered somethin you might be interested in this topic: So far I haven't tried it but it looks exactly like the thing I need for this. And something special: Version 1 of the Lyfe Main Theme is done. (Lyfe_Main.mp3) That's it for this week. I want to focus on the cell stage for this devblog as long as I'm working on it. This might result in shorter entries but we want to keep you up to date. We're also still looking for people to join the team if you're interested. UPDATE: I managed to create an actual cube with a procedural mesh UPDATE 2: We go some actual random shapes that will later function as the outline for the clouds.
  9. Lyfe

    The Cell Stage - Gameplay

    We're currently using GIT Hub. So far we don't have anything fancy to show. The time I creates the project page for Lyfe is also about the same time we started working. Don't expect too much, we're working on this during our spare time after all. A picture of the current state is on the project page. Note that it's mostly placeholder art since we're still looking for someone dedicated wholly to creating the art assets and we've been focusing on code so far.
  10. For this weeks update I wanted to talk about something I'm currently working on (conceptually and actually in code): The Cell Stage and how its gameplay works. Disclaimer: I'm not promising anything I'm just explaining the plan we have for Lyfe and we will work hard to get as close as we can to that vision. Secondly Some of the info you read here might not yet be in the DesignDocument but it will be updated after this post. You start out as just a membrane with some DNA in it. Floating around one of the possible bioms. Different bioms have different spawn rates for different chemicals. We haven't yet fully worked out what chemicals will be there in detail but the basics are: Oxygon, Carbon (mostly as CO2 or simple Sugars), Hydrogen (since you are under water there is quite a lot of that so it might not necessary be a ressource), Nitrogen (as Ammonium), Phosphorus (organic Phosphorus), Sulfur. Since these are the building blocks for life. Since at the beginning you are not really a living organism yet you don't need any of it. You don't have any real means of propulsion at this point, too. You can simply wiggle around a bit and move forward this way or get carried around my the currents of whatever body of water you are currently in. At this point the game is like a low key cutscene. This changes when you meet your first other cell. It will weither be a "Proto-Mitochondrium" or Cyanobacterium. The biom you are in has influence on this. If you are somewhere deep in the ocean cyanobacteria might be more scarce since... you know... it's dark down there and photosynthesis doesn't work so well without light. As you might have guessed this unlocks the Mitochondrium or Chloroplast. (The other one can be unlocked later one as you move to other bioms over time.) Both of them can produce ATP which basically is the value of how much energy you have. The Mitochondrium uses sugar and oxygon to produce ATP while the Chloroplast uses CO2 (since Hydrogen might not be a ressource). To balance this the Chloroplast produces less ATP. As long as you have those chemicals left energy will be produced. Other organelles you unlock on your microscobic journy will consume it to give you an advantage in surviving. For example if you got a flagellum or cilia on your cell and move forward ATP will be consumed over time. The more organelles are on your cell the more is consumed. This will force you, the player, to think about what you really need to survive. Some organelles you might unlock include: Mitochondrium -> Produces ATP Chloroplast -> Produces ATP (but less) Vacuole -> Store more compounds Cell Core -> better defense agains viruses Cell Wall -> More health but slower Endoplasmic Reticulum -> Repairs Cell Golgi Body -> Produces Vesicel => Reduced cost for ATP production since compounds get transported faster Flagellum -> Movement Ribosome -> Produces protein => your acquire DNA faster Poison Vacuole -> Damages cells attacking you <Poison Excretion> -> Lets you excret poison to damage enemy cells Sensory Input -> Allows you to "see" Cilia -> Movement You might now have some questions like "what's it with allowing me to see?" So basically at the beginning - and I can't stress enough how much this is just the plan we're TRYING to follow - you don't really see what's around you. You can make out your surroundings as basic shapes. This part is basically some artsy-o vision through you see the game world. The simple reason we decided to add this is that having no eyes has a penalty in the creature stage and a consistant ruleset is an important factor in this, so it feels like one game. But we also didn't just want to make a black screen because that isn't fun. (Also we're up for suggestions on how we could solve this better.) Then: DNA. You acquire it through absorbing other cells. The same way you can unlock new organelles. The criterium that decides if you absorb the cell or it absorbs you is how much mass you have. One simple rule that works like one browsergame proofed. And apart from other cells viruses might also be a factor in gameplay. This is also more of a experimental-feature on which I'd like to hear your opinions. If you spot a virus you can swim away from it absorb it. If you pick the latter one of two things can happen: You gain a bonus or a malus for this "life". They are gone as soon as you die. Splitting your cell doesn't remove it. This neatly brings us to the next point: Splitting your cell and the editor. All I want to say about it in the cell gameplay is that you need a certain amount of DNA to split and open the editor. We want it to be something the player has to work for. After all Lyfe is supposed to be a game. Now we come to the control settings which aren't that essential to this entry but I'm currently working on them so I just want to throw this in here: The player can chose from one of three settings: Follow Mouse Move To Click WASD I think I mentioned all I need to make clear how this part of the game works except for one thing and we're still struggling with that: How will you, the player, progress through this stage. Of collect all the organelles but those just give you options to adapt to your environment but what is the overall goal you are working towards? For the other stages this was easy to decide because there are other games to look for examples. But for this part? Not even our big inspiration Spore really had something interesting in this place. It was basically pacman but with a full 360° range of motion. We want to improve on that. But as I said we don't have a 100% bullet proof idea, yet. As always we're open for suggestions. That's it for this week from the Lyfe team and me. Thanks for reading.
  11. Lyfe

    Introduction - Organ Tree

    Ok,ok. I get what you mean. So far we plan on having the movement calculated from your creature's bodystructure BUT also add different types of legs that give you +1 speed (just to give an example; maybe we might also just restrict that to the 'campaign') because it makes sense from a gameplay point of view. Just to answer that point because it's something important to me, too. About removing amitious features: We are not a company. We don't have deadlines. This has it's ups and downs. For one we are a small team doing this because we share a common vision of what could be which might slow down development. On the other hand we have all the time to publish it and can stay faithful to that vision. We just want to make the best version of this game we can. I want to promise a lot but know I can't.
  12. For our first dev blog entry we decided to introduce you to the organ-tree which we already described in our design document. If you haven't read you can find it in the description of our project here. So about that organ-tree: This is it: The tool you use during the creature stage to unlock or as it will be called ingame "evolve" your organs/skin types/body parts. Now, this is still a very rough outline but I think the intention is clear. One important design decision we made is to split it into three categories: Aquatic, Terrestrial and Both, with you starting off mostly in the aquatic branch, since your life (or lyfe if you will) as a complex creature will start off in the beautiful and mysterious depths of the ocean. Parts from the aquatic part of the tree might be useless on land or even harm you and vice versa. For example you don't want to leave the water with gills on your creature because you will run out of oxygen very quickly. This doesn't apply to all parts, though. For aquatic hands for example it is more of a design choice (We might add gameplay functionalities later like aquatic hands help with swimming but aren't good for attacking while with some hands developed from the terrestrial branch you can claw at your rivals or prey). You might have noticed that every set of organs has one base set and four versions deriving from it with numbers at the end. Each new version will be a new model/have a new design and slightly different stats. We are not 100% sure if we can keep the 4 stages of each organ but we will try. Because you know what they say: "Shoot for the moon. Even if you miss you will end in the vacuum of space and die alone." What's next? The skin types. This is something where we really want to make use of the advanced technology we have compared to that from Spore almost ten years ago. Skin might not have an effect on the gameplay (yet, we're planning something for our nice-to-haves) but will make your creature look fantastic. The plan was keep it a small number of types but make them well differentiated. Eyes: Withou eyes you won't be able to see anything, pretty straight forward, BUT: How good your eyes are determines how well you see objects at a distance/how far you can see. As a little bonus we also added a branch that grants you nightvision if you plan on making a nocturnal creature. Finally: The brain. This is an indicator for your prograss in the creature stage. You start off with the first and most basic brain. You have to unlock all three upgrades to be able to enter the next stage which we will undoubtedly discuss at some point in this blog.
  13. Lyfe

    Introduction - Organ Tree

    It will. Let's say you got your first set of eyes on your creature. With those it can see about 20m (or something, game play tests will tell). After you evolve (unlock) the second set you also have to add them to your creature to benefit from their increased stats.
  14. Lyfe

    Introduction - Organ Tree

    We want to keep our development as transparent as possible to not generate unnecessary hype the game can't live up to. Although we are still a small team now we work on expanding to get the necessary resources to make this vision come true. And with the advancement in game engines since Spore we are fairly optimistic we will improve on a lot of its shortcomings.
  15. Lyfe

    Introduction - Organ Tree

    Thanks for the reply. We will definitly do our best and some more. What do you mean with 'advanced techniques'? With regard to what Spore did? Just asking so we know what people whant from this.
  • Advertisement

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!