Intermediate C++ Game Programming Tutorial 25

From Chilipedia
Revision as of 04:39, 13 February 2020 by R vdw (Talk | contribs) (Video Timestamp Index)

Jump to: navigation, search

Function pointers allow you to store and change what function is to be called dynamically at runtime. Combine that with containers to have collections of actions/behaviors that can be selected from, and then jam in the power std::function to allow you to wrap up pretty much any kind of callable thing and bring them together in one container. Groovy.

Topics Covered

  • Function pointers
  • Functionoids
  • std::function

Bonus Video

  • Pointers to member functions
  • std::bind
  • std::mem_fn
  • std::reference_wrapper (a little)
  • Pointer to static member function (it's the same as for normal funcy bois)
  • You can use normal func pointers for lambdas if they do not capture anything

Video Timestamp Index

Tutorial 25


  • How to switch on a string? (execute some code based on the string passed to the switch): using Functionoids 0:15
  • One way to do this is with a map to polymorphic functors
  • The Functionoid Pattern (objects that encapsulate function pointers) looks like this:
#include <iostream>
#include <memory>
#include <unordered_map>
#include <string>

struct CaseFunction
{
    virtual void operator()() const = 0;
};
struct SixtyNine : public CaseFunction
{
    void operator()() const override;
    {
         std::cout << "The S number\n";
    }
};

int main()
{
    std::unordered_map<std::string,std::unique_ptr<CaseFunction>> sw;
    sw["sixty-nine"] = std::make_unique<SixtyNine>();
    (*sw["sixty-nine"])();
    return 0;
}
  • But Chili is not a fan of this:
- awkward syntax to invoke the switch,
- need to revert to unique pointer protection
- forced to put data on the heap
- need to code derived classes for new functors
- function definitions are seperated from the point in the code where we map them
+ On the plus side: provides good type safety
  • Introducing Function Pointers 5:13
  • Idea is similar to pointing to data (stores the address of the data in memory
- Pointer to function stores the address to the first instruction of a function in memory
- Functions are just a collection of instruction bytes in memory. If you jump to the memory address of the first instruction, you essentially execute the function
  • Declaring a function pointer: int(*pFunc)(int);. pFunc can now point to any function that takes an int and returns an int, and is invoked with ()
int Double( int x )
{
    return x * 2;
}

int main()
{
    int ( *pFunc )(int);
    pFunc = Double;
    /*...*/
    std::cout << pFunc( 2 ) << std::endl;
}
  • Now you can point to different functions and invoke them through one single pointer dynamically (at runtime)
  • Using Function Pointers with a map 7:27
void SixtyNine()
{
    std::cout << "The S number\n";
}

int main()
{
    std::unordered_map<std::string,void(*)()> sw;
    sw["s9"] = SixtyNine;
    sw["s9"]();
}
  • Chili's opinion of this solution:
+ Nicer switch semantics
- But with less type safety, sw maps to any function with this signature
- While this is a good solution, you still need to declare a new function for every case in the switch
- Function definitions are still seperate from where we actually map the cases to the strings
  • Alternative route: use std::function<> in order to map to lambda functions 9:33
  • Problem is, every lambda function is its own seperate type, and we have to give a single type for all values in the map
  • In order to get this to work, we need to #include <functional> so that we can use the type std::function
  • std::function<> is a wrapper around "all callable things" and bring them all under one single polymorphic interface. They hold callables with a specific signature
  • In our example, you use it like so: std::function<void()>, where void() is the actual type of the function (in this case, functions that take no parameters and return void)
  • It's like the function pointer syntax void(*)() (which is a pointer to a function), but without the (*)
  • Now we can assign lambdas to the switch that maps to that function type
...
#include <functional>

int main()
{
    std::unordered_map<std::string,std::function<void()>> sw;
    sw["sixty-nine"] = []()
    {
        std::cout << "The S value";
    }
    sw["sixty-nine"]();
}
  • Finally, Chili is happy:
+ We have our function definition in the same place where we map it
+ We don't need seperate function definitions
+ std::function is the way forward!
  • It allows us to bring different lambda functors (that all have different types) into the same container.
  • Note that we can also pass the other function definitions to the map through std::function (the functionoid and function pointer definitions)
  • Example: making a single string switch class 13:01
  • Implementation of the StringSwitch class:
#pragma once
#include <functional>
#include <unordered_map>
#include <string>
#include <iostream>

class StringSwitch
{
public:
    std::function<void()>& Case(const std::string& str)
    {
        return map[str];
    }
    std::function<void()>& Default()
    {
         return def;
    }
    void operator[](const std::string& str) const
    {
        auto it = map.find(str);
        if (it == map.end())
        {
            def();
        }
        else
        {
            it->second();
        }
    }
private:
	std::unordered_map<std::string, std::function<void()>> map;
	std::function<void()> def = [](){};
};
  • Note that you can do useful things that you couln't do with a normal switch:
- It's an object, so you can pass it around to different functions
- You can access variables outside of the switch (by capturing values, [] in the lambda definition)
- You can have switches that take multiple parameters (() in the lambda definition)
- Your switches can return values
  • Homework assignment: time to face the music!! 20:06

Tutorial 25 - Bonus

Tutorial 25 - Solution

Homework Assignment

This homework might wreck your butthole a little, but hopefully not too badly. Anyways, you get the code from the repo, you try and get it to build. The project requires Box2D, but the repo doesn't have it included so you're gonna need to wrangle it yourself. The easiest way to accomplish this is to pull in dependencies into your system with vcpkg. Some of you are probably going to run into some speed bumps at this point getting vcpkg to work on your system, but I recommend powering through and not giving the fuck up, because vcpkg is immensely useful for adding amazing libraries to your projects easily. If you search YouTube, you'll find a video of me showing how to install vcpkg and use it to grab SFML, so that might be a good starting point.

After you get that shit working, the party has just started. Then you need to figure out how the codebase actually works. Use all the tools at your disposal (debugger, Google, Box2D documentation, etc.), and don't get bogged down in unimportant distractions (you don't need to know how the triangle rendering pipeline works to understand the general simulation system, for example).

The actual tasks are, as laid out in the video:

  1. Implement destruction of boxes when two boxes with the same color trait hit each other
  2. Implement a box splitting mechanic
  3. Implement a pattern matching collision event manager based on std::unordered_map (this is the main task that ties into Intermediate 25)
  4. Use the pattern matching system together with box splitting and other effects to define simulation with various action that occur based on what colors of boxes collide with each other.

Chili will hook you up with a solution video that A) explains the starting codebase in some detail and B) goes over the process of implementing all the the above.

The repo: GitHub

See also