Thursday, October 31, 2019

Quackit Tutorials

https://www.quackit.com/

Unity : FPS Microgame Analysis -1-

Unity released very good FPS example for people and I decided to analysis how they make this. Personally I wanted to show you how I analysis the games.

Class: Player Input Handler


What Player Input Handler class does is controlling a player. It reference to GameFlowManager and get the state of current game to ignore user input when game is finished.

To Change Weapon
If user press 1, 2, 3 and so on, we can get positive number and it is used in PlayerWeaponsManager class.

Class: Player Character Controller

PlayerCharacterController class controls player’s character. Many properties are adjustable to game feel better. It uses PlayerWeaponsManager to change weapon and Health Class. Health Class is managing health for player and bots.(Both uses Health Class)

Class: PlayerWeaponsManager

At the beginning of the game player should have weapon. Variable ‘startingWeapons’ contains initial weapon list.

Weapon camera
Weapon has its own camera to render the weapon on top of main camera. Default position of weapon uses DefaultWeaponPosition. When User Press zoom (which is aiming), it will use AimingWeaponPosition. Its x value is 0 which means aligned to 0 of x.

Aiming
When user press right button, Aiming is activated. As soon as aiming is activated then all of camera’s FOV(field of view) is changed. This effect will make zoom in. FOV Animation is code driven which means changing FOV value is running by code.

void UpdateWeaponAiming()
{
    if (m_WeaponSwitchState == WeaponSwitchState.Up)
    {
        WeaponController activeWeapon = GetActiveWeapon();
        if (isAiming && activeWeapon)
        {
            m_WeaponMainLocalPosition = Vector3.Lerp(m_WeaponMainLocalPosition, aimingWeaponPosition.localPosition + activeWeapon.aimOffset, aimingAnimationSpeed * Time.deltaTime);

            SetFOV(Mathf.Lerp(m_PlayerCharacterController.playerCamera.fieldOfView, activeWeapon.aimZoomRatio * defaultFOV, aimingAnimationSpeed * Time.deltaTime));
        }
        else
        {
            m_WeaponMainLocalPosition = Vector3.Lerp(m_WeaponMainLocalPosition, defaultWeaponPosition.localPosition, aimingAnimationSpeed * Time.deltaTime);
            SetFOV(Mathf.Lerp(m_PlayerCharacterController.playerCamera.fieldOfView, defaultFOV, aimingAnimationSpeed * Time.deltaTime));
        }
    }
}
In here, we use aimingWeaponPosition and defaultWeaponPosition.


To display crosshair hud
At the end of update function, raycast and set the isPointingAtEnemy bool variable. This variable is used by CrosshairManager which will display crosshair hud based on isPointingAtEnemy.

<Raycast fail and no enemy>

<Raycast success then CrosshairManager will show different hud>

Weapon Change Animation
When user change the weapon, current weapon position moves down and changed 3d model (actually hide current weapon and show new weapon) and then move up. In this case, we use DownWeaponPosition.


 

Class: Jetpack

After user unlock Jetpack, user press spacebar then player can fly. A relationship between PlayerCharacterController and Jetpack is like down below.


Player’s movement uses characterVelocity. In every update frame of Jetpack checks and applying additional velocity into characterVelocity.

<<Gameplay Idea: Implement a skill ‘Relentless Pursuit’ of Lucian in LOL or Tracer’s Blink>>

Class: Actor


When Actor is activated, we register itself to ActorsManager actors container. ActorsManager doesn’t have any other logic. Actor also has no logic at all but it has aimPoint variable. This variable is for aiming. For instance bot is targeting a player then bot should have specific target. The target will use aimPoint of Actor class.

Class: WeaponController

Every weapon objects has WeaponController component. WeaponController represent weapon itself. Weapon type, decide which projectile should use, bulletSpreadAngle (for shotgun), aimZoomRatio and so on.

Zoom for sniper
For instance, we have sniper rifle then we probably has zooming function more than pistol. In that case, we can use aimZoomRatio. If we set animZoomRatio to be 0.3 for instance, then zoom is way bigger than 0.7

<aimZoomRatio 0.7>

<aimZoomRatio 0.3>

Firing
In WeaponController, we instantiate projectile prefabs. Function flow like down below.

HandleShootInputs -> TryShoot -> HandleShoot

Bullet position
Every generated bullets position set by weaponMuzzle. In addition, muzzle flash effect is generated.

Prevent firing too quickly
If we call HandleShoot function really quickly, bullets will be generated. If we call HandleShoot every single time when user press the button then bullets will be generated really quickly if user has lighting speed hand! We have to prevent it. There is a variable m_LastTimeShoot which will prevent calling a function really quickly. This is timing logic.


Class: EnemyController


Class: Damageable

There is a Health class, which represent health of Player or Bot. To decrease player/bot health, there are two ways.

1.     Call TakeDamage method of health class.
2.     Through Damageable, call TakeDamage method of health class.

Number 2 used for projectiles. Number 1 used for player itself. (get damage by falling/environment or other reasons)



That is it for today. Next time I'll look how projectile works (probably collision detection), add more features such as spawner for bot, user's new skills (tracer blink!)





Wednesday, October 30, 2019

Regression Analysis in Games.

I like Gameplay Programming. Why? Because change one value a little bit then feels very different. When I talk about the feel, I usually talk example of comparing Unreal Tournament and Quake series.

For instance jump. Feel of jump in UT and Quake are definitely different. Yes, they use similar vector calculation but different tune values.

There are another example which is Super mario. There are tons of different platformer games. but feel of jump in Super mario much better than other games. I don't know what values they use but I could use different method to make similar jump feel.

Using regression analysis, I could get a formula. First of all, you can visit http://physmo.sourceforge.net

Using PhysMo, you can get Mario's jump curve data such as


Time
Height
0
0
0.015683
0
0.033367
0.25974
0.05005
0.519481
0.066733
0.727273
0.083417
0.987013
0.1001
1.194805
0.116783
1.350649
0.133467
1.558442
0.15015
1.714286
0.166834
1.818182
0.183517
1.922078
...
...
0.500501
0.415584
0.517174
0.199842
0.523867
0
0.540551
0

With this data and use regression analysis in Excel then you can get the formula like below.





f(x) = -32.038x2 + 17.486x - 0.2101


with this f(x), we can get y value of Mario. it feels same as Super Mario. Of course we can't use it for Ground -> Air -> Falling situation and others but you get the idea how to use Regression Analysis in Games.


Reference
Original idea of Super Mario analysis by Kenton.

Tuesday, October 29, 2019

One pattern that I use on multi-threaded environment.

As many Unity developer already knows, Unity's logic runs in main thread. When you need network feature in your project you probably use socket or other network library. Of course when you send/receive some packets over the network, you will use non-blocking or asynchronize mechanism.

Normally we use callback or event function and those are called by OS. Ok then you can do whatever you want. Something like this.

void MyReceiveFunction(data)
{
    // ok, I received a data!
    if ( data == 0 )
    {
        foo(0);
    }
    else if ( data == 1001 )
    {
        foo(1001);
    }
    else
    {
        ...
    }
}

The problem is that the function is called on different thread. (which is not mianthread)

so If you do anything that it is related with main thread or GUI, logic won't run correctly. The pattern I'm going to talk about is saving this situation! (very simple)

void MyReceiveFunction(data)
{
    // ok, I received a data!
    if ( data == 0 )
    {
        reserveFoo(0);
    }
    else if ( data == 1001 )
    {
        reserveFoo(1001);
    }
    else
    {
        ...
    } 
}

in the reserveFoo function we can reserve the command like below.

void reserveFoo(int cmd)
{
    lock
    {
        reserveFooList.add(cmd);
    }
}

reserveFooList is a container. It store a cmd. and it is used in consumer logic in mainthread.


// Update function is called in main thread.
void Update()
{
    if ( reserveFooList.size > 0 )
    {
        foreach(element : reserveFooList)
        {
            Foo(element);
        }

        reserveFooList.clear;
    }
}

Now Update function is called in main thread and checks whether size of reserveFooList is greater than 0 which means it has some element to do.

if the list is not empty then iterate all the elements and process those in main thread. That's is the main idea.

Of course you can use invoke method for solve this issue but personally I prefer to do this.

Saturday, October 26, 2019

Every programming language has its own idioms.

Every programming language has its own idioms and idioms are not the same as Design Pattern. If you already know computer language such as C, C++, Java but you feel you are not good at it, then I suggest you to know/use/apply the idioms of the languages.
I'm sure you get the idea.

I should ignore domain specific problems, formula when I read books.

Formula are invented over time and tons of books talks about their domain specific problems, how to solve it. I read many different kind of books and tried to understand everything but I realized it is not possible. I should make some rule that ignore domain specific problems but understand roughly. fundamental is much important.

I think this rule can be applied to programming exercise too. I worked on gaming industry many years and worked on other industry too. I know audio programming, tools development, AI, compiler construction, mobile app development, reverse engineering, server development. even arduio programming. but I couldn't know everything.

1 : Concentrate on fundamental thing and ignore domain specific problems.
2 : Work on domain specific problems and solve it.
3 : Summarized.

Thursday, October 24, 2019

I like hackerrank because I can see other people's solution which is exactly same problem.

I've just solved 'Attribute Parser' problem on hackerrank. After I solved this problem and I had a look at the discussion to see other people's solution. I can see many good implementation!! I should learn from them. :) I think comparing other people's source code is really good way to think differently. My implementation is down below.


--------
#include <cmath>
#include <cstdio>
#include <vector>
#include <iostream>
#include <algorithm>
#include <string>
#include <stack>
#include <map>
#include <cstring>

using namespace std;

/*struct NameEquals
{
    bool operator() (const Tag& rhs) const
    {
        return name == rhs.tagName;
    }
    NameEquals(const string str) : name(str) {}
private:
    const string name;
};*/

class Tag
{
public:
    Tag(int _level, string data)
        : level(_level)
    {
        int index = data.find(' ');

        if (index != string::npos)
        {
            // get the tag's name not attribute name.           
            tagName = data.substr(1, index - 1);
            indexAftertagName = index;
            //cout << tagName << endl;
        }
        else
        {
            noattribute = true;
            tagName = data.substr(1, data.size() - 2);
        }
    }

    void readAttributes(string data)
    {
        // attributeName1 = "blabla" attributeName2 = "blablabla" ... >

        if (noattribute) return;

        int i = indexAftertagName + 1;
        const char* p = data.c_str() + i;

        while (true)
        {
            char ch = *p;
            if (isalpha(ch)) // attributeName
            {
                const char* attrP = p;
                while (*p != ' ') p++; // find out the empty
                const int len = p - attrP;               
                string attributeName = std::string(attrP, len);

                while (*p != '"') p++;

                const char* value = ++p;

                while (*p != '"') p++;

                const int valueLen = p - value;
                string attributeValue = std::string(value, valueLen);

                attributes.insert(make_pair(attributeName, attributeValue));
            }
            else
            {
                p++;
                if (*p == 0) break;
            }
        }
    }

    void addChildTag(Tag* newTag)
    {
        childTags.push_back(newTag);
    }

    void command(char ch, const char* data)
    {
        if (ch == '.')
        {
            // find child tag
            const char* childName = data + 1;
            const char* p = childName;
            while (*p != '.' && *p != '~') p++;
            string childTagName = std::string(childName, p - childName);
           
            bool foundTag = false;
            for (auto element : childTags)
            {
                if (element->tagName == childTagName)
                {
                    foundTag = true;
                    element->command(*p, p);
                    break;
                }
            }                     

            if ( !foundTag )
            {
                cout << "Not Found!" << endl;               
            }
        }
        else if (ch == '~')
        {           
            // find an attribute
            const char* pAttributeName = data + 1;
            const char* p = pAttributeName;
            while (*p) p++;
            string attributeName = std::string(pAttributeName, p - pAttributeName);
           
            auto elementIter = attributes.find(attributeName);

            if (elementIter != attributes.end())
            {
                cout << elementIter->second << endl;
            }
            else
            {
                cout << "Not Found!" << endl;
            }
        }
    }
   
    int level;
    int indexAftertagName;
    bool noattribute;
    string tagName;
    map<string, string> attributes;
    vector<Tag*> childTags;
};



/*
<tag1 value = "HelloWorld">
<tag2 name = "Name1">
</tag2>
</tag1>
*/

int main() {
    int n; // count of tags
    int q; // query
    cin >> n >> q;

    // skip escape char
    char c;
    c = cin.get();

    int level = 0;

    vector<Tag*> tags;
    stack<Tag*> stackTags;

    for (int i = 0; i < n; ++i)
    {
        string data;
        std::getline(cin, data);

        //cout << "read : " << data << endl;

        // checks whether it start with </ or <%alphabet
        if (data.at(0) == '<' && data.at(1) == '/')
        {
            // end of tag
            level--;
            stackTags.pop();
        }
        else
        {
            Tag* newTag = new Tag(level, data);

            newTag->readAttributes(data);
         
            if (stackTags.size() == 0)
            {
                tags.push_back(newTag);
                stackTags.push(newTag);
            }
            else
            {
                // add this tag as a child
                stackTags.top()->addChildTag(newTag);
                stackTags.push(newTag);
            }


            level++;
        }
    }

    for (int i = 0; i < q; ++i)
    {
        string data;
        std::getline(cin, data);
       
        const char* p = data.c_str();
        const char* tagNameP = data.c_str();;
        while (*p != '.' && *p != '~') p++;

        const string tagName = std::string(tagNameP, p - tagNameP);

        //auto iter = std::find_if(tags.begin(), tags.end(), NameEquals(tagName));
        bool tagFound = false;
        for (auto element : tags)
        {
            if (element->tagName == tagName)
            {
                tagFound = true;
                element->command(*p, p);
                break;
            }
        }

        if ( !tagFound)
        {
            cout << "Not Found!" << endl;           
        }
    }


   
    return 0;
}






Wednesday, October 23, 2019

Dynamic Programming...

Well... be honest I never use DP in my whole career. I know benefit of using Dynamic Programming(DP) to solve specific problems but I had no chance to use it in my career. Maybe it's is because I wasn't familiar with or had no opportunity. Recently I'm reading some books which talks about DP. Probably I'll talk about DP soon. :)

Sunday, October 20, 2019

about pointer assignment operation.

I never thought about it because I just knew how it works. Today I'll show you why pointer assignment statement in function is not working as intended.

Just assume there is a simple Node class like below.

class Node
{
public:
int data;
};

this Node class will contain integer data variable as a member and has nothing more. Normally Node class has prev, next pointer member but I'll ignore those in this article.

OK, let's create an head and give it a value 10.

Node* head = new Node();
head->data = 10;

now head pointer will point to some address which is sizeof(Node)

to see the address of head. we can do this.

cout << head << endl;   <--- 1
cout << &head << endl; <--- 2

line 1 will print the address of sizeof(Node) which contains Node's value(integer 10)




If you are using Visual Studio then you can use memory viewer and see the content of the memory like below.


As you can see 0x00526ED0 has integer value of 10. Second address which is address of head pointer. 0x002CF718.

Just think about what is there.



D06E5200 is an reverse order of address 0x00526ED0. This is because intel CPU uses little endian.

Anyway 0x002CF718 which is &head, contains the address of sizeof(Node) (actual value)

now if we pass head pointer to other function and assign new Node. see what happen.


void foo(Node* p)
{
cout << p << endl;
cout << &p << endl;

Node* item = new Node();
item->data = 1024;

p = item;
}


As you can see third address is same as sizeof(Node)'s address. but forth address is different. that's address of p pointer!

when we call foo function and pass head pointer as a param like below.


foo(head);

p is a copy of head pointer which is different variable. their target address which is 0x00526ED0 are same but address of itself are different. so somebody wants to change head's target address like this.


p = item;


it is not working properly. if you see memory window then you will see what's changed.


after p = item statement is executed then address will be changed.


ok p's target address is changed but the problem is that address of p and address of head are different! so basically head's target address is not changed.

if we exit foo function then effect(changing address) will be gone. because p = item is actually changed value of p pointer's address.

This is really trivial thing for C/C++ programmer but cumbersome.

if we want to change head pointer's target address then we should pass pointer of pointer of head variable which is Node**

void foo(Node** p)
{
Node* item = new Node();
item->data = 1024;

*p = item;
}

and use it like this.

foo(&head);

or there are different way to change address of head which is returning Node* in the function foo.

Node* foo(Node* p)
{
Node* item = new Node();
item->data = 1024;

return item;
}

head = foo(head);

Simple LRUCache class implementation.

This is not the best way to implement LRU Cache class. I just implement it for hackerrank problem. Concept of LRU Cache is not difficult but implement doubly linked list was a little bit hard without compiler :^O

------
// inheritance from the Cache class
class LRUCache : public Cache
{
public:
    LRUCache(int _capacity)
        : capacity(_capacity)
        , count(0)
    {
        head = nullptr;
        tail = nullptr;
    }

    void set(int key, int value) override
    {
        auto iter = mp.find(key);
        bool keyExist = false;
        if (iter != mp.end())
        {
            keyExist = true;
        }

        // if key is exist then update LRU
        if (keyExist)
        {
            // for instance there are 3 2 1 5 6
            // and then found 1
            // data will be 1 3 2 5 6
            // doesn't change any data size, just update head, tail pointer and map.

            if (head == iter->second)
            {
                // update value.
                iter->second->value = value;
            }
            else if (tail == iter->second)
            {
                Node* beforeTail = tail->prev;
                Node* afterHead = head->next;

                Node* backupHead = head;
                head = tail;
                tail = backupHead;

                beforeTail->next = tail;
                tail->prev = beforeTail;
                tail->next = nullptr;

                afterHead->prev = head;
                head->next = afterHead;
                head->prev = nullptr;

                // update map pointer and value.
                iter->second = head;
                iter->second->value = value;
            }
            else
            {
                // value is in between head and tail.
                Node* backupPrev = iter->second->prev;
                Node* backupNext = iter->second->next;
               
                backupPrev->next = backupNext;
                backupNext->prev = backupPrev;

                head->prev = iter->second;
                iter->second->next = head;

                head = iter->second;
                               
                iter->second = head;
                iter->second->value = value;
            }
        }
        else
        {
            // if key is not in the map then insert it and based on the capacity
            // remove oldest one.
            if (count >= capacity)
            {
                // insert new one into head
                Node* node = new Node(key, value);
                node->next = head;
                head->prev = node;

                head = node;
                mp.insert(make_pair(key, head));

                // remove old one
                // first remove old one in the map and then update tail pointer.           
                Node* tailPrev = tail->prev;
                tail->prev->next = nullptr;               
                mp.erase(tail->key);       
                delete tail;
                tail = tailPrev;
            }
            else
            {
                // add new one and update count
                if (head == nullptr)
                {
                    head = new Node(key, value);
                    tail = head;
                    mp.insert(make_pair(key, head));
                }
                else
                {
                    // update head
                    Node* node = new Node(key, value);
                    node->next = head;
                    head->prev = node;
                    head = node;

                    mp.insert(make_pair(key, head));
                }
                count++;
            }
        }
    }

    int get(int key) override
    {
        // if the key is in the cache then print the value
        // otherwise print -1 (if the key is not in the cache)
        auto iter = mp.find(key);
        if (iter != mp.end())
        {
            return iter->second->value;
        }
        return -1;
    }
       
private:
    int capacity;
    int count;
};

Saturday, October 19, 2019

Keep author or translator is really hard.





I like share things what I've figured out to other programmers. I wrote many programming books, translated game programming books in South Korea. but well... be honest I got few things and lost many things. I started to write/translate books because it helps me develop myself and wanted to be helped to korean gaming industry. But recently I realized that I should change a method.

It took 20 years! I'll show you what is a new method later :)

Task in UnrealEngine

 https://www.youtube.com/watch?v=1lBadANnJaw