Jump to content

  • Log In with Google      Sign In   
  • Create Account

Hodgman

Member Since 14 Feb 2007
Online Last Active Today, 07:13 PM

#5208286 Alternatives to Entity Component design

Posted by on 02 February 2015 - 04:54 PM

What I would like to know is what alternatives are there to ECS and how do they compare?

On most professional games I've worked on - just normal OOP, where you use composition instead of inheritance. Your components are based on this idea anyway, just without the fancy buzzword...

The most common non-ECS approach you're likely to find is the one that helped catalyze the adoption of a component-based approach to modelling entities in games in the first place: the deep class hierarchy, where everything inherits from some common base class and every time new functionality or behavior is required of an entity, a new subclass is created off of the most-similar existing class

AKA terrible 90's "OOP" code :lol:

The main idea of ECS - that you should always use composition to solve everything, is very similar to one of OO's core tennants - to prefer composition over inheritance.

From what I saw in the 90, people moved away from these horrible inheritance trees towards generic components because they wanted things to be more data-driven. C/C++ are static languages, but these teams wanted something more like SmallTalk, etc, so the implemented some data-driven aspect-oriented programming and messaging frameworks, which would allow game designers to create new 'classes' of game objects without requiring a programmer.

These days, ECS now seems to be popularised instead by the new wave of DOD interest - that this model won't necessarily be more data-driven and designer friendly, but the concern is that it will give better performance.

This new focus for ECS may itself be another reaction to OO architectures... Every console game that I've shipped on the most recent 2 generations of consoles has, instead of making the language dynamic via an ECS framework, has just used a dynamic language! e.g. Composition-based OO in Lua, which indeed is slow. This performance issues was managed by re-writing any performance critical parts (e.g. large loops over many objects) as multi-core C++ code (DoD and regular OO).


#5208176 Clean up and what to keep in mind

Posted by on 02 February 2015 - 06:53 AM

Organisation of the codebase is always something to consider when cleaning up
The bible and related links:
http://stackoverflow.com/questions/1860796/your-thoughts-on-large-scale-c-software-design


#5208135 What kind of math do you use to balance games?

Posted by on 01 February 2015 - 11:27 PM

Statistics, lots of statistics biggrin.png

 

At one past job, game designers would basically have an entire implementation of the game rules in an Excel spreadsheet, tweaked to perfect balance, including information on volatility and demographic appeal, before the programmer even started implementing the game. blink.png (this is not common practice in my experience though!)




#5208114 Light-weight render queues?

Posted by on 01 February 2015 - 06:49 PM

I've got a scene and a renderer. In order to draw models, my scene iterates all its high-level models, materials, etc and compile it into a structure (my renderqueue) and is sent of to the renderer which draws it. It has all it needs to draw the scene.

struct RenderableMesh
    struct RenderableMaterial
    struct RenderableModel
    struct RenderableCamera
...

All those structures are very specific, removing flexibility from what can possibly be drawn using the back-end.
e.g. The per-mesh constant data, or the number and names of the per-material textures are all hard-coded.
If later you're implementing a technique where a mesh needs more values, or its own look-up-table texture, or same for a material, etc, you're stuck... Ideally you want the data that makes up a material (the number of textures, their names, etc) to be completely data-driven instead of being hard-coded.

I prefer to keep my back-end completely generic and unaware of what the high level data is. No hardcoding of materials, lights, shader inputs, etc...
//Device has resource pools, so we can use small ids instead of pointers
ResourceListId: u16
TextureId : u16
CBufferId: u16
SamplerId: u16

//n.b. variable sized structure
ResourceList: count, TextureId[count]

DrawState: u64 - containing BlendId, DepthStencilId, RasterId, ShaderId, DrawType (indexed, instanced, etc) NumTexLists, NumSamplers, NumCbuffers.

//n.b. variable sized structure
InputAssemblerState: IaLayoutId, IndexBufferId, NumBuffers, BufferId[NumBuffers]

DrawCall: Primitive, count, vbOffset, ibOffset, StencilRef. 

//n.b. variable sized structure
DrawItem: DrawState, InputAssemblerState*, TexListId[NumTexLists], SamplerId[NumSamplers], CbufferId[NumCbuffers], DrawCall 

DrawList: vector<DrawItem*>
A generic model/mesh class can then have a standard way of generating it's internal DrawItems. A terrain class can generate it's DrawItem in a completely different way.
Debug UI's can freely generate DrawItem's on demand for immediate mode style usage, etc...

I build sort keys by combining some data from the DrawState(u64) with a hash of the DrawItem's resource arrays.


#5207830 Parallax Corrected Cube Maps

Posted by on 31 January 2015 - 04:52 AM

For complex environments, I'm use signed distance fields as the proxy, and sphere-tracing through the field to find the intersection point, which is used for correcting the cube-map lookup direction.


#5207784 Some programmers actually hate OOP languages? WHAT?!

Posted by on 30 January 2015 - 07:37 PM

Carmack felt more comfortable with C.
They used C++ for Doom 3, but it was a learning experience for them.

C is definitely less popular in games than C++ now, but is likely still used by many.
In 2010 I worked at a massive studio who wrote their engine (comparable with UE3 in scope) in plain C, and then the gameplay code in C++.

This is the same methodology as Half-life 1 back in 1998. :D

Almost every other games job that I've had has used C++, C# and Lua though.
However, when writing "systems code", as you often are in game engines, it's common to write C++ code that looks very much like C code ;)


#5207671 Is it important to update your apps?

Posted by on 30 January 2015 - 07:07 AM

Companies like Supercell (who make clash of clans) have full-time staff members who's entire job is to analyze the data collected from player's devices and guess why people are and aren't spending money, and then suggesting psychological experiments that can be carried out on their player base to confirm their hypothesis and discover how the game can be subtly tweaked to make it more addictive profitable.

 

They aren't successful simply because they publish updates. Publishing updates is just a side-effect!




#5207666 LGPL ugliness and LZMA

Posted by on 30 January 2015 - 06:49 AM

The 7-Zip application is LGPL, but the LZMA SDK is public domain.




#5207627 Profiling tools

Posted by on 30 January 2015 - 01:26 AM

You can write your own and embed it into your application fairly easily.

Or use an Open Source library that does it for you like google-perftools.

You can either modify that to suite your needs, or use it to help learn how to write your own. It's not as hard as it sounds.

^^This.
I'm outputting my timings in Chrome format at the moment, which removes the need for me to write a visualizer. Was pretty simple to get multiple CPU cores + the GPU showing N frames worth of timing bars myself.

I haven't used this one but it looks handy too: https://github.com/Celtoys/Remotery


#5207584 Some programmers actually hate OOP languages? WHAT?!

Posted by on 29 January 2015 - 07:18 PM

Still... what might "well-coded" games in C look like?

May as well check out the work of one of the original rock star gamedevs:
https://github.com/id-Software/Quake-III-Arena
http://fabiensanglard.net/quake3/index.php
http://fabiensanglard.net/quake2/index.php
http://fabiensanglard.net/quakeSource/index.php
http://fabiensanglard.net/doomIphone/index.php
http://fabiensanglard.net/doomIphone/doomClassicRenderer.php


#5207413 Efficient way to erase an element from std::vector

Posted by on 29 January 2015 - 05:44 AM

Wouldn't it be enough to test for std::is_trivially_copyable? Requiring the whole more restrictive POD-property seems unnecessary.

Yep. I forgot that C++'s definition of POD is now stupidly restrictive. I usually use "POD" to mean "memcpy-safe", which apparently C++ calls "trivially copyable" now sad.png




#5207407 Triangles can't keep up?

Posted by on 29 January 2015 - 05:31 AM

Now for this format I assume I should choose one that supports my highest Vec* value used in my shader. But what happens if
I have mixed Vec* values? E.G vec3 for position and just a float for particle lifespan. Would I need to use filler dummy values to offset or will it automatically be 0 if nothing is specified?

You can use a vec4 to hold a position + a lifespan.
It doesn't really matter though as long as each element is 4-byte aligned. i.e. a vec3 + a float will be fine as a 12-byte and a 4-byte element. Alternatively you can use a 16-byte vec4, but it's much the same.
If you're using smaller types though, it can end up being more vital to pack things together.
e.g. a 16-bit x 3, 8-bit x 1, 8-bit x 2, 8-bit x 3 are unusable vertex/pixel formats, as they result in bad alignments.
Strangely, 16-bit x 1 seems to be the exception, where 2-byte alignment is allowed.
 

While we are on the subject of TBOs. I implemented these for the rendering portion of my system using a single TBO and a IBO. This kind of got me thinking, I know there is a technique to just use textures for updating particle states, so is there a point to using transform feedback? I know with transform feedback I can use the GPU for processing while the CPU does its own thing, but can't I do this with just using textures too? Or am I only limited to updating the texture through the CPU in a glBufferData call?

Yeah holding the state in a texture is completely valid too. In this technique, you'd have a large 2D texture (or many large textures) holding the state of each particle.
e.g. if you required 8 floats to hold a particle's state, and you had 1M particles, you could use 2 textures that were each RGBA_FP32 and 1024*1024.

To update the particles, you'd need to double-buffer the data, as you can't read/write a texture simultaneously using the graphics pipeline.
Youd bind one set of your textures to an FBO, and then render a quad that fills the viewport (covers all 1024*1024 pixels), reading texture-set A, computing state, and outputting to texture-set B.
Next update you'll read from B and write to A, etc...

To draw the particles, you render a million quads, and use their quad ID (vertex ID / 4) to generate a texture coordinate, then read from your textures in the vertex shader to get the particle's properties.

n.b. Instead of using the graphics pipeline, on modern (DX11/GL4) GPUs, you could store the particle state in a buffer or texture, and update it using a compute shader, which is allowed to read+write the same resource!

Some older (DX9/GL2) GPUs may support reading from textures in the vertex shader, but might not support transform-feedback - so this technique could be a possible fallback for those GPUs.
Other DX9/GL2 GPUs don't support VTF (reading textures within vertex shaders) nor do they support transform-feedback... but some support a weird extension known as R2VB (render to vertex buffer) that lets you bind a vertex buffer to an FBO, so the pixel shader is actually outputting vertices!

So:
1 - GL2/DX9 A) No way to have the GPU generate vertices -- CPU generated particles only.
2 - GL2/DX9 B) Has R2VB -- Fragment shader writes particle state to a vertex buffer.
3 - GL2/DX9 C) Has VTF -- Fragment shader writes particle state to a texture, vertex shader reads values from texture.
4 - GL3/DX10) Has transform-feedback -- Vertex shader writes particle state to a vertex buffer.
5 - GL4/DX11) Has compute -- Compute shader writes particle state to a generic buffer (or texture).

#2 era also supports #1
#3 era also supports #1
#4 era also supports #1, #3
#5 era also supports #1, #3, #4


#5207401 Efficient way to erase an element from std::vector

Posted by on 29 January 2015 - 05:10 AM

template<typename ContainerType>
void SwapAndPopAtIndex(ContainerType & container, size_t index)
{
    if (index + 1 != container.size())//#####################
        std::swap(container[index], container.back());
    container.pop_back();
}

Could you just remove the highlighted line, assuming that if an item is expensive to swap, it will contain it's own early-out inside a if-swapping-this-with-this test?

Out of curiosity, does anyone know if there is any chance the swap can be optimized to a move when the items don't have destructors?
I guess that could be difficult unless the memory in the vector is actually shrunk..
Not that it should really matter, but for large POD types it seems unnecessary to exchange the memory instead of just copying.

C++11 introduced a POD testing template, so you could write:
if( std::is_pod<T>::value )
  container[index] = container.back();
else
  std::swap(container[index], container.back());



#5207397 Education vs Industry Experience

Posted by on 29 January 2015 - 05:03 AM

¿por qué no los dos?
Can you defer your studies and resume your degree after the year of contract work? Or complete it part time while working?
Personally, I got my first job half way through my degree (moving cities in the process) and finished my degree slowly via part time long-distance education.
 
Experience is more valuable than education... but education is often used as a first pass filter to cut down the number of applicants for a job.


#5207394 Some programmers actually hate OOP languages? WHAT?!

Posted by on 29 January 2015 - 04:49 AM

Ravyne, for returning multiple values from a function or method I generally use std::pair and similar. This isn't the most efficient though as it obviously has to be copied, or allocated in the function and freed outside of it which has always felt wrong to me...

The C++ standard allows the compiler to skip the copy constructor that should in theory be invoked when returning an object by value. Compilers have taken advantage of this for a decade, so in most situations, returning a large object from a function is completely free.
 
e.g. if you write:
struct Widget { int a,b,c,d; }
Widget Foo() { Widget temp = {1,2,3,4}; return temp; }
void Test() {
  Widget w = Foo();
}
The compiler can automatically transform it into:
struct Widget { int a,b,c,d; }
void Foo(Widget* result) { *result = Widget{1,2,3,4}; }
void Test() {
  Widget w;
  Foo(&w);
}
 
This could also be used as an illustration of how bad a language C++ is for being overly-complex though laugh.png Most programmers aren't aware of the subtle rules that dictate when their compiler will and won't make use of this optimization, so it's non-obvious whether a large return value is free or expensive...




PARTNERS