Author Topic: The Programming Thread  (Read 198 times)

0 Members and 1 Guest are viewing this topic.

Offline stands2reason

  • Empiricist, Positivist, Militant Agnostic
  • Poster of Extraordinary Magnitude
  • **********
  • Posts: 10746
The Programming Thread
« on: July 17, 2019, 07:02:02 PM »
Since it has come up a fair amount. There are a number of computer scientists & software developers here, myself included.

I'll start by talking about the last thing I worked on. I had been working pretty obsessively only my game project since November. After I got the release out, I switched over to refactoring and other cleanup. From about November to April, I was basically working on it all day every day. That's about my limit, I guess. About a week ago, I decided I need to take a vacation.

The last thing I worked on is switching over the physics engine and refactoring the physics callbacks.  In case you don't understand why C++ templates are awesome:

When you are doing physics callback dispatch in a game engine, the callback is going to give you a pair of objects (Box2D calls them "fixtures"). They are going to have "userdata" which is a void pointer. The object also have a numeric type ID; an enum bitset. It is your job, to do the dynamic magic, in the statically typed language, of using the type ID to figure out what kind of object. And you better get it right, because if static_cast a void pointer to the wrong type, all Hell breaks loose.

Well, not really. As you can imagine, I have a base game object type (GObject), and each physics types corresponds to a high level sub-class of GObject: Agent, Bullet, EnvironmentalObject, etc. I have been using C & C++ for about 12+ years now*.  I don't want to do anything that might produce a memory error; void pointers are a special joke among C/C++ programmers.^

Actually, the userdata is always a GObject, and I dynamic_cast. So there is this redundant boilerplate code in my physics callback functions:

Code: [Select]
void bulletBulletBegin(GObject* a, GObject* b, b2Contact* arb)
{
Bullet* _a = dynamic_cast<Bullet*>(a);
Bullet* _b = dynamic_cast<Bullet*>(b);

if (_a && _b) {
_a->onBulletCollide(_b);
_b->onBulletCollide(_a);
}
}

void playerPickupBegin(GObject* a, GObject* b, b2Contact* arb)
{
    Player* p = dynamic_cast<Player*>(a);
   
        if(auto c = dynamic_cast<Collectible*>(b)){
              p->onCollectible(c);
        }
else if (auto u = dynamic_cast<Upgrade*>(b)) {
p->applyUpgrade(u);
}
else if (auto inv = dynamic_cast<InventoryObject*>(b)) {
inv->onPlayerContact();
}
else if (auto _map = dynamic_cast<MapFragment*>(b)) {
_map->onAcquire();
}
}

void bulletEnvironment(GObject* bullet, GObject* environment, b2Contact* contact)
{
Bullet* _b = dynamic_cast<Bullet*>(bullet);
bool _sensor = environment->getBodySensor();
b2Manifold* manifold = contact->GetManifold();

if (_b && environment && !_sensor) {
if (!_b->applyRicochet(-1.0 * manifold->localNormal)) {
_b->onEnvironmentCollide(environment);

if (auto _hs = dynamic_cast<Headstone*>(environment)) {
_hs->hit(_b->getScaledDamageInfo().mag);
}
}
}
}

void bulletWall(GObject* bullet, GObject* wall, b2Contact* contact)
{
Bullet* _b = dynamic_cast<Bullet*>(bullet);
Wall* _w = dynamic_cast<Wall*>(wall);
bool _sensor = wall->getBodySensor();
b2Manifold* manifold = contact->GetManifold();

if (_b && _w && !_sensor) {
if(!_b->applyRicochet(-1.0 * manifold->localNormal))
_b->onWallCollide(_w);
}
}


With C++ templates, I could have the function signature (the type of each pair of object), used with automatic template deduction to do those dynamic_cast's for me, check that they are successful (and log warning otherwise) all in one place. Like this:

Code: [Select]
//Actually do dynamic_cast.
template<typename T, typename U>
pair<T*, U*> getCastObjects(b2Contact* contact)
{
GObject* a = static_cast<GObject*>(contact->GetFixtureA()->GetUserData());
GObject* b = static_cast<GObject*>(contact->GetFixtureB()->GetUserData());

return make_pair(
dynamic_cast<T*>(a),
dynamic_cast<U*>(b)
);
}

//Just a helper to get my physics type, casted back to its enum type.
pair<GType, GType> getFixtureTypes(b2Contact* contact)
{
GType typeA = static_cast<GType>(contact->GetFixtureA()->GetFilterData().categoryBits);
GType typeB = static_cast<GType>(contact->GetFixtureB()->GetFilterData().categoryBits);

return make_pair(typeA, typeB);
}

template <typename T, typename U>
PhysicsImpl::contact_func makeObjectPairFunc(void(*f)(T*, U*, b2Contact*), PhysicsImpl::collision_type types)
{
return [f, types](b2Contact* contact) -> void {
auto crntTypes = getFixtureTypes(contact);
T* t = nullptr;
U* u = nullptr;

if (types == crntTypes) {
tie(t, u) = getCastObjects<T, U>(contact);
}
else if (isReverseMatch(crntTypes, types)) {
tie(u, t) = getCastObjects<U, T>(contact);
}

if (t && u) {
f(t, u, contact);
}
};
}


As you can see, the first two functions are simply helpers. I am storing the objects as a pair, since the physics engine doesn't guarantee anything about which the first or second. I use the convention that the numerically lower physics enum type is always first. I didn't bother to copy isReverseMatch; you can guess how that works.

The real cool bit here is that makeObjectPairFunc takes a function pointer, but is templated according to the type of the first two arguments to that function. i.e. I can change the type signature of my callbacks so that they take the type of object they actually need, and this template function now does all of that boilerplate:

Code: [Select]

void bulletBulletBegin(Bullet* _a, Bullet* _b, b2Contact* arb)
{
_a->onBulletCollide(_b);
_b->onBulletCollide(_a);
}

void bulletWall(Bullet* _b, Wall* _w, b2Contact* contact)
{
bool _sensor = _w->getBodySensor();
b2Manifold* manifold = contact->GetManifold();

if (!_sensor) {
if(!_b->applyRicochet(-1.0 * manifold->localNormal))
_b->onWallCollide(_w);
}
}

//...and so on...

It actually returns a std::function. At some point, I have to put all of my callbacks in a hash table (std::unordered_map), keyed by pair(GType,GType)

Code: [Select]
#define _addHandler(a,b,begin,end) \
AddHandler( \
make_pair(GType::a, GType::b), \
makeObjectPairFunc(&begin, make_pair(GType::a, GType::b)), \
makeObjectPairFunc(&end, make_pair(GType::a, GType::b)) \
)

#define _addHandlerNoEnd(a,b,begin) \
AddHandler( \
make_pair(GType::a, GType::b), \
makeObjectPairFunc(&begin, make_pair(GType::a, GType::b)), \
nullptr \
)

void PhysicsImpl::addCollisionHandlers()
{
_addSensorHandler(playerGrazeRadar, enemyBullet);
_addSensorHandler(enemySensor, bomb);
_addSensorHandler(enemySensor, enemy);
_addSensorHandler(enemySensor, enemyBullet);
_addSensorHandler(enemySensor, player);
_addSensorHandler(enemySensor, playerBullet);

_addHandler(player, enemy, playerEnemyBegin, playerEnemyEnd);
_addHandlerNoEnd(player, enemyBullet, playerEnemyBulletBegin);
_addHandlerNoEnd(playerBullet, enemy, playerBulletEnemyBegin);
}

* They were among the first language I used, but during the middle, there were a few years where I hardly used them. In a couple of years, I'll have been doing it for half of my life.
^ In case you're wondering, yes I did once create an object with the wrong corresponding physics type; I got a nice log output warning me what happened, and the callback was simply not run.
« Last Edit: July 17, 2019, 07:29:41 PM by stands2reason »

Offline stands2reason

  • Empiricist, Positivist, Militant Agnostic
  • Poster of Extraordinary Magnitude
  • **********
  • Posts: 10746
Re: The Programming Thread
« Reply #1 on: July 17, 2019, 07:27:52 PM »
Well, the physics engine was the last major thing I worked on, but I did get my first piece of work done in about a week. I had also spent some time looking at branches / stashes of things I had started on. That's what Box2D conversion was, for a few months. As was this:

I had already refactored the logging system, so that it was thread-safe. It also now printed out to a log file as well stdout. But it is doing it in the main (graphics thread). Obviously, doing any kind of I/O, even printing to stdout, is not a good thing to do in a graphics/game program, because there is a chance it will suspend the thread. So, I basically followed the template I used for setting up concurrency with my game-logic thread, and make a file I/O thread. It gets a notification signal (std::condition_variable), so it isn't regularly waking up and polling; only when LogSystem::logOutput is called. So, it runs in the background, only when it needs to, taking the I/O out of the main thread.

Yes, you have to use two different mutexes (muticies?). One is for the container (vector), so that either thread can access it. The other is for the condition variable itself. Because what happens if thread suspends, it has to edit condition_variable internal variable to say it is suspending on condition, at the exact same time that other thread calls logOutput and calls notify on on mutex. I think it can be done with a re-entrant mutex, but why bother if it isn't the right solution.

Code: [Select]
void LogSystem::logOutput(const string& s)
{
bufMutex.lock();
buf.push_back(s);
bufMutex.unlock();

threadCondVar.notify_one();
}

void LogSystem::initThread()
{
std::string path = FileUtils::getInstance()->getWritablePath() + std::string("log.txt");
outputFile.open(path, std::ofstream::app | std::ofstream::ate);

logThread = thread(&update);
}

void LogSystem::exit()
{
exitFlag.store(true);
logThread.join();
}

void LogSystem::update()
{
while (!exitFlag.load())
{
unique_lock<mutex> mlock(threadCondMutex);
threadCondVar.wait(
mlock,
[]() -> bool { return !buf.empty() || exitFlag.load(); }
);

if (!exitFlag.load())
{
std::vector<std::string> _outbuf;
bool _print = false;

bufMutex.lock();
swap(_outbuf, buf);
bufMutex.unlock();

for (string s : _outbuf)
{
outputFile << s << std::endl;
printf("%s\n", s.c_str());
}
}
}
}

mutex LogSystem::bufMutex;
vector<string> LogSystem::buf;
ofstream LogSystem::outputFile;

thread LogSystem::logThread;
condition_variable LogSystem::threadCondVar;
mutex LogSystem::threadCondMutex;
atomic_bool LogSystem::exitFlag;

That's the basic skeleton for setting up a background task thread using C++11 concurrency.

Offline Calinthalus

  • Too Much Spare Time
  • ********
  • Posts: 6571
    • My Page
Re: The Programming Thread
« Reply #2 on: July 18, 2019, 07:49:56 AM »
Since I've been doing this professionally for 12 years or so, I have quit programming anything at all in my off hours.  My eyes and hands are going bad, so I don't want to push it.  At 46 I understand why people say programming is a young man's game.


At the office most all of my work is done in .NET or doing non-programming tasks with some tools from our partner(s).  Most of the complex parts of my job are trying to figure out what people are really wanting instead of what they are saying.  That's supposed to be the PM's job, but alas he just copypastas whatever the clients say.


I also do support on our different products inhouse which involves fixing things people have broken between files and databases and FTP sites etc.etc.  Been having a lot of fun with LinqPad lately.  It allows me to program in C# directly into a database, do file manipulation, accesses Nuget packages etc.etc. without having to create an actual .NET project/solution.  Kinda cool.
"I think computer viruses should count as life. Maybe it says something about human nature, that the only form of life we have created so far is purely destructive. Talk about creating life in our own image."
--Stephen Hawking

Offline superdave

  • Too Much Spare Time
  • ********
  • Posts: 6235
Re: The Programming Thread
« Reply #3 on: July 18, 2019, 08:36:23 AM »
this is beyond my level but looks fun!
I disavow anyone in the movement involved in any illegal,unethical, sexist, or racist behavior. However, I don't have the energy or time to investigate each person and case, and a lack of individual disavowals for each incident should not be construed as condoning such behavior.

Offline stands2reason

  • Empiricist, Positivist, Militant Agnostic
  • Poster of Extraordinary Magnitude
  • **********
  • Posts: 10746
Re: The Programming Thread
« Reply #4 on: July 18, 2019, 09:04:33 AM »
I also do support on our different products inhouse which involves fixing things people have broken between files and databases and FTP sites etc.etc.  Been having a lot of fun with LinqPad lately.  It allows me to program in C# directly into a database, do file manipulation, accesses Nuget packages etc.etc. without having to create an actual .NET project/solution.  Kinda cool.

At one point, I was doing training for enterprise Java/Oracle web development. Oracle-SQL also has its own programming language that is used to program event handlers in the database (can't remember the technical term). Since it's Oracle, you would expect it to use Java right? No. It uses PL/SQL, which is basically just a souped-up Pascal.

I think it was only briefly brought up as a joke, the trainer definitely seemed to think so, because who would program in Pascal nowadays? I wasn't totally lost though: one of my CS classes, system software & compilers, actually had us make a compiler/VM for a simplified version of Pascal.

That is not the way lexical scoping is supposed to work. ::) At least with C++ lambdas, you can specify (overall or per variable) if you want to capture by value or by reference.

Offline Calinthalus

  • Too Much Spare Time
  • ********
  • Posts: 6571
    • My Page
Re: The Programming Thread
« Reply #5 on: July 18, 2019, 09:22:18 AM »
I took Pascal in high school believe it or not.


I had to do some work in an Oracle database several years back.  A client wanted some input/output from an Oracle db which they wouldn't give me access to for dev/testing.
Being an old hand at SQL (MySQL and MS) I thought to myself, "how different can it really be?"  Apparently very.  I did get it working eventually, and honestly it had some features I wish were available elsewhere.  But without dumping out a ton of money on training classes and books it is really difficult to teach yourself Oracle.
"I think computer viruses should count as life. Maybe it says something about human nature, that the only form of life we have created so far is purely destructive. Talk about creating life in our own image."
--Stephen Hawking

Online brilligtove

  • Too Much Spare Time
  • ********
  • Posts: 7617
  • Ignorance can be cured. Stupidity, you deal with.
Re: The Programming Thread
« Reply #6 on: July 18, 2019, 09:48:41 AM »
Most of the complex parts of my job are trying to figure out what people are really wanting instead of what they are saying.  That's supposed to be the PM's job, but alas he just copypastas whatever the clients say.

That's a Business Analyist's job. Good PMs and good Devs tend to develop those skills, but it's not usually part of the role description. It's certainly not in the PMBOK. You may be interested in checking out your local chapter of IIBA.org. Business Analysis is quite rewarding.

ETA: Also intensely frustrating, but that's why it's the most complex part of your job already. :)
evidence trumps experience | performance over perfection | responsibility – authority = scapegoat | emotions motivate; data doesn't