Info
This post was imported from a personal note. It may contain inside jokes, streams of consciousness, errors, and other nonsense.
Having a generic listener for when entities are created and deleted will reduce contact points between Simulation and SimRenderer. After I convert references to Boid to use IAgent instead it’ll make it easier for me to add different Boids, like the upcoming BraitenBoid class with its two-sensor setup.
When SimRenderer, an IEntityListener, is notified about entities created in the simulation, it needs to respond differently to Boids and FoodSources. This lead me to create an EntityType enum. Here’s the switch statement that determines the entity type and casts the IEntity to the implementing class.
void SimRenderer::entityCreated(IEntity &entity) {
EntityType entityType = entity.getEntityType();
switch (entityType) {
case EntityType::Boid:
boidCreated(*(dynamic_cast<Boid *>(&entity)));
break;
case EntityType::FoodSource:
foodSourceCreated(*(dynamic_cast<FoodSource *>(&entity)));
break;
default:
// TODO: Never throw a string literal. Convert to exception class.
throw "Unexpected entity type.";
}
}
I’m glad I figured out how to use an enum class
. I much prefer using EntityType::Boid
rather than just Boid
(which doesn’t work because it conflicts with the actual boid class name) or having to rename it to BoidEntityType
or something.
But look at that line for casting the IEntity to a Boid. Oof. That ugly. So I get the address of the entity which is a pointer to an IEntity. Then I cast it to a pointer to a Boid. Then I dereference the pointer so I’m dealing with just a Boid again.
To make sure I wasn’t copying the Boid instance, I added some log output to the Boid constructor and ran the app. Each Boid was instantiated once for each generation or load action so no, I wasn’t duplicating Boid construction. Who knows what other laws of nature I’m breaking but it seems to be correct..?
I really need to find some one to review my code. Or find time to read a C++ book. (-:
shared_from_this() #
Man, cppreference.com is great. The example of Good, Best, and Bad practices is so clear.
#include <iostream>
#include <memory>
class Good : public std::enable_shared_from_this<Good>
{
public:
std::shared_ptr<Good> getptr()
{
return shared_from_this();
}
};
class Best : public std::enable_shared_from_this<Best>
{
struct Private{};
public:
// Constructor is only usable by this class
Best(Private) {}
// Everyone else has to use this factory function
// Hence all Best objects will be contained in shared_ptr
static std::shared_ptr<Best> create()
{
return std::make_shared<Best>(Private());
}
std::shared_ptr<Best> getptr()
{
return shared_from_this();
}
};
struct Bad
{
std::shared_ptr<Bad> getptr()
{
return std::shared_ptr<Bad>(this);
}
~Bad() { std::cout << "Bad::~Bad() called\n"; }
};
void testGood()
{
// Good: the two shared_ptr's share the same object
std::shared_ptr<Good> good0 = std::make_shared<Good>();
std::shared_ptr<Good> good1 = good0->getptr();
std::cout << "good1.use_count() = " << good1.use_count() << '\n';
}
void misuseGood()
{
// Bad: shared_from_this is called without having std::shared_ptr owning the caller
try
{
Good not_so_good;
std::shared_ptr<Good> gp1 = not_so_good.getptr();
}
catch (std::bad_weak_ptr& e)
{
// undefined behavior (until C++17) and std::bad_weak_ptr thrown (since C++17)
std::cout << e.what() << '\n';
}
}
void testBest()
{
// Best: Same but can't stack-allocate it:
std::shared_ptr<Best> best0 = Best::create();
std::shared_ptr<Best> best1 = best0->getptr();
std::cout << "best1.use_count() = " << best1.use_count() << '\n';
// Best stackBest; // <- Will not compile because Best::Best() is private.
}
void testBad()
{
// Bad, each shared_ptr thinks it's the only owner of the object
std::shared_ptr<Bad> bad0 = std::make_shared<Bad>();
std::shared_ptr<Bad> bad1 = bad0->getptr();
std::cout << "bad1.use_count() = " << bad1.use_count() << '\n';
} // UB: double-delete of Bad
int main()
{
testGood();
misuseGood();
testBest();
testBad();
}
Boid → IAgent #
Simulation maintains a list of Boids. If I change that to IAgent, the draw() calls are okay but CollisionDetection is gonna be a problem. Each type of IAgent should be allowed to have its own collision detection code. Shapes are gonna be different.
Need to quickly read up on polymorphism again. I think I mentioned this before but can I just implement a few CollisionDetection methods and have it (what it? it’s not gonna be the compiler) figure out which to call? Or am I gonna have to do a switch on IAgent.getAgentType()
here, too?
“Late binding” “dynamic polymorphism” “runtime polymorphism”
Apparently what I’m looking for is called “Multiple Dispatch.” Not 100% sure on that yet. I might have to see if the code works or not. If I can pass a Boid that’s an IAgent to CollisionDetection::detect(Boid &boid, FoodSource &fs)
then it’s not called Multiple Dispatch becaus C++ is said not to have Multiple Dispatch.
'CollisionDetection::detect': no overloaded function could convert all the argument types
^ I have my answer.
CollisionDetection Options #
So one option is CollisionDetection takes pairs of IEntity and figures out their specific entity type and figures out if they’re colliding. This is easier to add new types but means these entity type calls and casting will be done a lot.
Another option is for Simulation (or someone) to maintain separate lists for each entity type and iterate over those. This makes it harder to add new types because with each type a new list needs to be maintained. But then the correct collision detection method can be called directly. It also means things that don’t collide or that have no effect on collision don’t need to be tested. Downside of that is the Simulation has some knowledge of which objects to test collisions on and which to skip. Theoretically that should be up to the entities themselves and the CollisionDetection class.
I guess the CollisionDetection class could have some methods bool isCollidable(EntityType a, EntityType b)
and Simulation can run collision detection just on those pairs of types but blah, that’s hugely overcomplicating things.
Having to implement copy all these when I add BraitenBoid is kinda silly, though:
std::vector<std::shared_ptr<Boid>> boids;
void addBoid(BoidProps boidProps);
void clearBoids();
std::vector<BoidProps> getBoids();
void setBoids(std::vector<BoidProps> boidPropses);
void resetBoids();
I guess I’ll need BraitenBoidProps, too.
Huh.
Isn’t the goal down the road to have it possible for populations to gradually change their form and behaviour over generations, possibly dividing into separate species as they adapt to different niches in the environment? In that case, having explicit classes representing different species (like Boid and BraitenBoid) goes against one of the (long term) goals of this project.
Adding BraitenBoids goes against BraitenBoids. d-:
What would I have me do? Ideally, the Boid implements both sensor models, one where two neurons represent the nearest food’s relative location in polar coordinates and one where two neurons represent any food’s location relative to each of a pair of sensors. Then, a gene determines which sensor setup this particular boid has invested in. Then either they get a strong signal from one and a weak or noisy signal from the other and see where evolution takes us.
I was planning to have these compete in isolation. Use one sensor setup and run a bunch of generations and measure performance, then do the same with the other sensor setup. Tough to say whether I would get different results out of these.
Implementing separate Boid and BraitenBoid classes sounds much easier. And maybe for this early stage it’s a better way to go. That way I can directly compare the two sensor setups against each other. I’ll have numbers I can report on (“BraitenBoids pick up food 3x faster than polar Boids”) instead of, at best, “most simulations converge toward the Braitenberg sensor setup in 5-10 generations.”
I’ll mull it over.