반응형

C++ 의 std::function 을 이용하여 코드를 사용하기 쉽게 변경하기.


std::Function은 무엇인가

함수 포인터    =  반환값이 같은 타입이여야 하고 코드위치를 가르키기 때문에 메모리를 할당,회수가 불가하지만 빠르다.

 

std::Function  =  암시적 형변환이 가능하고

                           일반적인 함수포인터와 다르게 람다함수나 bind 또한 맴버함수에 대한 포인터도 사용가능하다

 

선언방법

int Func(int n){ cout<<n<<endl; };

std::function<void(int)> funcName1 = Func; //함수 대입
std::function<void(int)> funcName2 = [](int a) {}; // 람다함수
std::function<void(int)> funcName3 = std::bind(Func, 1); //std::bind

 

사용방법은 일반 함수와 같다

funcName1(10);

 

사용하는 이유

cpp로 player을 개발하던 도중 enum swich case 를 여러번 사용하는 경우를 보았다.

 메인업데이트 부분의 player 상태 처리와 렌더링부분의 player 상태처리 처럼  업데이트 상황을 나눠서 계산할때

state를 계속 추가해주었다.

업데이트 루프마다 플레이어의 상태들을 관리해 주는것은 player의 상황이 늘어났을경우 각종업데이트에서

swich를 써둬서 상황을 생성할때마다 swich문을 추가하는것과 커플링의 문제가보였다.

 이러한 문제는 코딩을 할때 일이 많아지고 코드의 길이가 길어져 비효율적이다.

state 패턴

 swich case를 state 패턴처럼 이용하며 함수들을 좀더 유동적으로 다루기위해

객체를 등록하여 사용이 필요했다.

위의 사진처럼 swich 문과 update를 하나의 루프에 묶어서 돌린다.

 

하나의 클래스에서  처음시작할때 원하는 위치에 함수를 받아서 저장 해둔뒤 실행하면 state에 따라 실행하게 고안하였다.

다른 오브젝트를 새로 생성하거나 제작할때 오브젝트의 init 함수에 특정 event를 걸어주게 만들면 알아서 돌아갈것이다.

 

장점

이런식으로 구조를 제작하면 새로운 상황을 추가할때 추가해주는 코드가 줄어들고

한쪽에서 코드를 지워도 문제없이 코드가 실행될것이다.

또한 하드코딩이 아니라 프로그램 내에서도 코드를 유동적으로 사용가능할것이다.

 

단점

유동적인 코드가 한번에 확인하기 어렵고, 필요한 순간 할당 제거를 할수있기에

나뉘어서 동작하는 함수들을 많이 추가하는 경우 서순이 꼬여서 생기는 찾기 어려운 오류가 생길수있다.

 

 

테스트 코드

//테스트 함수 추가를 위한 player 클래스
class Player
{
	
public:
	Player(int c) : b(c) { cout << a<<endl; };
	int b = 21;
	void playerMove( );
	void playerIdle( );
private:
	int a =10;

};

void Player::playerMove( )
{
	this->a+=this->b;
}

void Player::playerIdle( ) {
	cout << a<<endl;
}
//std::function 을 이차원 배열로 생성
class Deleg {
public:
	std::vector<std::function<void()>> Deleg;
};

//플레이어의 상태만큼 미리 event update의 크기를 만들어둘 예정
enum PlayerState 
{
	Move,
	Idel,
	Skill,
	StateCount
};



int main() {
	//Render update,LateUpdate,등 업데이트 순서
	const int UpdateRate= 2; 
	Player p = {Player(4)};
  	Player* _P = &p;
  
  Deleg StateDelg[PlayerState::StateCount][UpdateRate] = { { {} ,{} } ,{ {} ,{} } ,{ {}, {} } };
	StateDelg[0][0].Deleg.push_back([=]() {cout << endl; _P->playerMove(); cout << endl;  });
	StateDelg[0][1].Deleg.push_back([_P]() {_P->playerMove(); _P->playerIdle(); });


	StateDelg[1][0].Deleg.push_back([_P]() {_P->playerMove(); _P->playerIdle(); });
	StateDelg[1][1].Deleg.push_back([_P]() {_P->playerMove(); _P->playerIdle(); });


	StateDelg[2][0].Deleg.push_back([_P]() {_P->playerMove(); cout << "1Update End" << endl; });
	StateDelg[2][0].Deleg.push_back([_P]() {_P->playerMove(); cout << "1Update End" << endl; });
	StateDelg[2][0].Deleg.push_back([_P]() {_P->playerMove(); cout << "1Update End" << endl; });
	StateDelg[2][0].Deleg.push_back([_P]() {_P->playerMove(); cout << "1Update End" << endl; });
	StateDelg[2][0].Deleg.push_back([_P]() {_P->playerMove(); cout << "1Update End" << endl; });

	StateDelg[2][1].Deleg.push_back([_P]() {_P->playerMove(); _P->playerIdle(); });
  
  
  //전체 코드를 실행하지만 i나 j를 바꿔줌으로써 특정 state만 실행시켜줄수있다
	for (int i = 0; i < StateCount; i++)
		for (int j = 0; j < UpdateRate;j++) {
			for (int k = 0; k < StateDelg[i][j].Deleg.size(); k++) {
				(StateDelg[i][j].Deleg[k]());
			}

		}
  
 }

 

위 코드는 테스트 코드로써

1. 이차원 배열[플레이어 상태][ 업데이트 ] 를 만들어주었다.

    역으로 업데이트종류 마다 지정된 플레이어 상태에 맞는 함수를 실행시켜 주어도 좋다.

2. 함수(애니메이션 스크립트,스킬등)를 시작할때 원하는 [상태 또는 업데이트] 에 맞는 함수 리스트에 넣어준다.

3. 상황에 맞게 돌린다. 

 

위 소스코드의 결과 값이다.

 

이제 다른 함수들이 특정 state부분만 잠시 실행시키기 원해도 배열의 위치만 맞게 실행시켜주면 간단하게 사용가능하다.

'언어 > C++' 카테고리의 다른 글

CPP 콘솔 미로 만들기  (0) 2023.11.15
cpp 전처리기  (0) 2023.09.05
함수 포인터! (Function Pointer)!  (0) 2021.07.13
인라인 함수(Inline Function)  (0) 2021.06.15
네임스페이스  (9) 2021.05.18
반응형

unity ObjectPool API 
(Version:2021)

이 글을 읽기 전에 자신이 쓰는 유니티 버전을 확인해보라

유니티가 지원하는 objectpool은 2021년 이후 버전부터 추가되었다.

 

1.ObjectPool 이란?

2.Unity ObjectPool

3. 글쓴이가 응용한 방식


 

1.ObjectPool

우리가 사용 하려는 오브젝트 풀은 간단하게 설명하면

오브젝트를 담아두고 재활용하기 위한 디자인 패턴이다.

 

어째서 재활용을 해야할까? 라는 생각을 할 수 있다.

이 물음에 답하자면

 

 생성과 파괴보다 재사용이 빠르고 가볍다.

 

생각해보라 우리가 오브젝트를 "생성" 한다는것은 그냥 그 자리에 나오는 것이 아닌

생성-> class에 맞는 데이터 크기만큼 메모리 할당-> 재생성시 재 대입해줘야 하는 데이터 값

그리고 파괴할 때 가비지 메모리 제거 등 많은 작업이 필요하다.

 

이러한 작업이 적을 경우에는 크게 상관이 없을 수 있지만 많은 데이터를 생성과 제거를 반복한다 생각하면 끔찍하다.

 

이러한 작업을 적게 할 방법이 ObjectPool이라는 방식이다.

 

ObjectPool은 오브젝트를 (생성-> 사용-> 제거)반복 에서  생성-> 사용-> (보관->재사용)반복 ->제거 식으로 사용하여

메모리를 아끼는 방식이다.


 

2.Unity ObjectPool

 

 

 

 

 

 

 

 

 

 

 

Pool 관련 클래스는 몇 가지가 있지만. 대부분 비슷한 사용법을 가진다.

그중에서 ObjectPool <T0>을 사용할 것이다.

namespace UnityEngine.Pool
{
    //
    // 요약:
    //     A stack based Pool.IObjectPool_1.
    public class ObjectPool<T> : IDisposable, IObjectPool<T> where T : class
    {
        public ObjectPool(Func<T> createFunc, Action<T> actionOnGet = null, Action<T> actionOnRelease = null, Action<T> actionOnDestroy = null, bool collectionCheck = true, int defaultCapacity = 10, int maxSize = 10000);

        public int CountAll { get; }
        public int CountActive { get; }
        public int CountInactive { get; }

        public void Clear();
        public void Dispose();
        public T Get();
        public PooledObject<T> Get(out T v);
        public void Release(T element);
    }
}

 

이게 내부 구성과 ObjectPool의 매개변수다.

ObjectPool(생성할 때 함수, 사용할 때 함수, 사용 끝났을 때 함수, 제거 함수, 충돌 체크, 기본적으로 생성해둘 양, 최대 양)

으로 생각하면 쉽다


 

총과 총알로 설명하겠다.

먼저 총알 prefab에 IobjectPool <GameObject> pool {get;set;}을 생 성해준 뒤 Update에 날아가는 코드를 작성해준다.

using UnityEngine.Pool;

public class Bullet : MonoBehaviour
{
    public IObjectPool<GameObject> Pool { get; set; }
    
    public virtual void ReleaseObject() => Pool.Release(this.gameObject);
    public virtual void OnTakeFromPool(GameObject obj) { }
    public virtual void OnReturnedToPool(GameObject obj) => obj.SetActive(false);
    public virtual void OnDestroyPoolObject(GameObject obj) => Destroy(obj);


}


public class Bullet_1 : BulletBase
{
    public float speed = 7;
    private void Update()
    {
        if (transform.position.y >= 10)
        {
            ReleaseObject();
        }
        transform.Translate(speed * Time.deltaTime * Vector3.up);
    }

    public override void OnTakeFromPool(GameObject obj)
    {
        obj.SetActive(true);
    }
}

 

그 후 BulletSpawner.cs를 만들어주어  ObjectPool <Gameobject>  Pool를 만들어준 뒤

ObjectPool내부에 생성할 Bullet의 함수들을 넣어둔다.

그리고 bullet 생성 함수에서 bullet 내부의 IObjcetPool pool에 spawn의  Pool을 넣어준다.

( bullet 내부에서 spawn의  Pool을  사용해야 하기 때문에) 

 

그리고 특정 상황마다 BulletSpawner 에서  Pool.Get();을 해준다.

 Pool.Get()은  Pool 내부 함수로 자동으로 현재 재사용 가능한 오브젝트가 있는지 없는지를 판단하여

생성해준다.

public class BulletSpawner : MonoBehaviour
{
 
    public BulletBase poolablePrefab;
    public IObjectPool<GameObject> Pool { get; private set; }
    

    public void Init()
    {
        
              Pool = new ObjectPool<GameObject>(
              delegate {
        // 오브젝트 생성함수
        // 오브젝트 생성
        GameObject obj = Instantiate(poolablePrefab.gameObject);
        //생성한 오브젝트의 IObjectPool Pool 에 Spawner의 ObjectPool을 넣어준다.
        obj.GetComponent<BulletBase>().Pool = Pool;
            return obj;
        },
        // Bullet에 구현된 사용할 함수들 입력
        poolablePrefab.OnTakeFromPool, poolablePrefab.OnReturnedToPool,
        poolablePrefab.OnDestroyPoolObject,
        true, poolablePrefab.defaultCapacity, 50));

    }
    
    public void Shoot()
    {
       var a= poolablePrefab.Pool.Get();
        a.transform.position = this.gameObject.transform.position;
    }
    private void Start()
    {
        Init();
        InvokeRepeating("Shoot", 0f,5f);

    }
}

 

 

 

 

 

아래는 유니티에서 제공한 예제다

더보기
using UnityEngine.Pool;

// This component returns the particle system to the pool when the OnParticleSystemStopped event is received.
[RequireComponent(typeof(ParticleSystem))]
public class ReturnToPool : MonoBehaviour
{
    public ParticleSystem system;
    public IObjectPool<ParticleSystem> pool;

    void Start()
    {
        system = GetComponent<ParticleSystem>();
        var main = system.main;
        main.stopAction = ParticleSystemStopAction.Callback;
    }

    void OnParticleSystemStopped()
    {
        // Return to the pool
        pool.Release(system);
    }
}

// This example spans a random number of ParticleSystems using a pool so that old systems can be reused.
public class PoolExample : MonoBehaviour
{
    public enum PoolType
    {
        Stack,
        LinkedList
    }

    public PoolType poolType;

    // Collection checks will throw errors if we try to release an item that is already in the pool.
    public bool collectionChecks = true;
    public int maxPoolSize = 10;

    IObjectPool<ParticleSystem> m_Pool;

    public IObjectPool<ParticleSystem> Pool
    {
        get
        {
            if (m_Pool == null)
            {
                if (poolType == PoolType.Stack)
                    m_Pool = new ObjectPool<ParticleSystem>(CreatePooledItem, OnTakeFromPool, OnReturnedToPool, OnDestroyPoolObject, collectionChecks, 10, maxPoolSize);
                else
                    m_Pool = new LinkedPool<ParticleSystem>(CreatePooledItem, OnTakeFromPool, OnReturnedToPool, OnDestroyPoolObject, collectionChecks, maxPoolSize);
            }
            return m_Pool;
        }
    }

    ParticleSystem CreatePooledItem()
    {
        var go = new GameObject("Pooled Particle System");
        var ps = go.AddComponent<ParticleSystem>();
        ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);

        var main = ps.main;
        main.duration = 1;
        main.startLifetime = 1;
        main.loop = false;

        // This is used to return ParticleSystems to the pool when they have stopped.
        var returnToPool = go.AddComponent<ReturnToPool>();
        returnToPool.pool = Pool;

        return ps;
    }

    // Called when an item is returned to the pool using Release
    void OnReturnedToPool(ParticleSystem system)
    {
        system.gameObject.SetActive(false);
    }

    // Called when an item is taken from the pool using Get
    void OnTakeFromPool(ParticleSystem system)
    {
        system.gameObject.SetActive(true);
    }

    // If the pool capacity is reached then any items returned will be destroyed.
    // We can control what the destroy behavior does, here we destroy the GameObject.
    void OnDestroyPoolObject(ParticleSystem system)
    {
        Destroy(system.gameObject);
    }

    void OnGUI()
    {
        GUILayout.Label("Pool size: " + Pool.CountInactive);
        if (GUILayout.Button("Create Particles"))
        {
            var amount = Random.Range(1, 10);
            for (int i = 0; i < amount; ++i)
            {
                var ps = Pool.Get();
                ps.transform.position = Random.insideUnitSphere * 10;
                ps.Play();
            }
        }
    }

 

 

3.

내가 구현한 방식이다.

 

 

Spawn에도 IObjectPool로 PoolManager에 BulletPool을 받아서 사용한다.
Manager 에서 새로운 오브젝트가 아닐경우 서로 공유해서 사용하게 한다.

 

글쓴이는 싱글톤으로 PoolManager을 만들어서 같은 총알은 서로 공유하여 사용하게 만들어보았다.

이런식으로 만들경우 Spawn컴퍼넌트를 오브젝트에 붙여주고 원하는 BulletPrefab을 넣어주면 완성이다.

 

참고자료 : https://docs.unity3d.com/ScriptReference/Pool.ObjectPool_1.html 

 

Unity - Scripting API: ObjectPool<T0>

Object Pooling is a way to optimize your projects and lower the burden that is placed on the CPU when having to rapidly create and destroy new objects. It is a good practice and design pattern to keep in mind to help relieve the processing power of the CPU

docs.unity3d.com

외 여러 영상들과 블로그

'엔진 > 유니티' 카테고리의 다른 글

Unity Spline 기능 추가!  (0) 2023.06.01
Unity Simulator  (0) 2023.01.20
unity Redis  (0) 2022.07.13
unity _Prefab Variants  (0) 2022.03.17
Unity Inspector 창 정리  (2) 2022.02.24
반응형

Unity 에서 Redis 서버를 사용해보자

 

Unity 버전 2019.4.15f1

 

NuGet의 StackExchange.Redis를 사용했다.

 


 설치&세팅

 

visual studio의 NuGet에서 Plugin을 다운받고 사용하면 사용이 안된다.

사용이 안되는 이유는 두가지로 

 

1. 유니티 내부에 있어야한다.

2. 유니티에서 호완하는 버전이 아니다

 

 

 

 

1. 누겟 페키지를 다운받았으면 다운받은 파일에 들어가서 

 

 

 

 

 

dll 파일과 xml 파일을 

 

unity Assets 파일에 Plugin 파일을 만들어서 넣어준다

 

 

 

 

 

 

 

 

 

 

 

2. 유니티 2019버전에서는 Redis 2버전대가 호환이 안된다

그럼으로 1.5버전을 다운받아서 넣어주면 된다.

 

 


사용법

 

 

그뒤로 unity 내부에서 사용하는 방법은 매우간단하다.

    private ConnectionMultiplexer redisConnection;
    private IDatabase DB;
    private ISubscriber sub;

3가지를 선언해 준다.

 

 첫줄은 StackExchange.Redis의 메인 객체다

 두번째 줄은 DB 데이터 를 주고받을 객체다.

 마지막줄 sub역시 데이터를 전달하기위해 있는객체다.

 

백문이 불여일견 코드로 보겠다.

Init Code

 public bool Init(string host, int port)
    {

        this.redisConnection = ConnectionMultiplexer.Connect(host + ":" + port);
        if (redisConnection.IsConnected)
        {
            this.DB = this.redisConnection.GetDatabase();
            sub = redisConnection.GetSubscriber();
            return true;
        }

        return false;
    }

    public bool Init(string host, int port, Events LoopEvents)
    {

        T = new Thread(new ThreadStart(LoopEvents));
        
        try
        {
            ConfigurationOptions option = new ConfigurationOptions
            {
                ConnectTimeout = 1000,
                AbortOnConnectFail = false,
                EndPoints = { $"{host}:{port}" },
            };
            redisConnection = ConnectionMultiplexer.Connect(option);
        }
        catch (Exception e)
        {
            Debug.Log(e);
        }

        if (redisConnection.IsConnected)
        {
            this.DB = this.redisConnection.GetDatabase();
            sub = redisConnection.GetSubscriber();
            T.Start();

            return true;
        }

        return false;
    }

 

위 코드의

        this.redisConnection = ConnectionMultiplexer.Connect(host + ":" + port);  줄에서 알수있듯

       "ip주소:포트넘버: 로 이루어져 있다. 

 

로컬로 테스트 할때는 아래 코드처럼 "localhost" 를 넣어서 사용하면된다.

this.redisConnection = ConnectionMultiplexer.Connect("localhost")

 

 그냥 주소로 connect를 시도 해도 좋지만

 

   ConfigurationOptions option = new ConfigurationOptions
            {
                ConnectTimeout = 1000,
                AbortOnConnectFail = false,
                EndPoints = { $"{host}:{port}" },
            };
            redisConnection = ConnectionMultiplexer.Connect(option);

이 부분처럼 ConfigurationOptions 를 사용하여 전달해도 좋다.

 그러면 구성 옵션을 세팅을 한번에 해줄수도 있다.

   

 

      this.redisConnection = ConnectionMultiplexer.Connect(host + ":" + port);

           ㄴ주소로 연결


        if (redisConnection.IsConnected) //연결 완료되었는가
        { 

       //연결 완료후 데이터들을 받는다.

            this.DB = this.redisConnection.GetDatabase();

            sub = redisConnection.GetSubscriber();

        }

 

 

 

 

이제 정상적으로 넘어간다면 다 끝났다. 

데이터를 주고받는 방법은 간단하다.

 

 

 

 

 

 

 

 

 

 

 

아까 선언해주었던 DB 객체의 내부에 전부 들어있다. 

아래 코드처럼 자신의 프로젝트에서 사용하기 편하게 만들어주면 끝이다.

 

   public string GetString(string key)
    {
        return this.DB.StringGet(key);
    }
    public bool SetString(string key, string val)
    {
        return this.DB.StringSet(key, val);
    }
    public RedisValue GetHASH(string key, string val)
    {
        return this.DB.HashGet(key, val);
    }
    public bool GetHASH_Bool(string key, string val)
    {
        var data = this.DB.HashGet(key, val);
        if (data.ToString()[0] == 't' || data.ToString()[0] == 'T')
        {
            return true;
        }
        return false;
    }
    public int GetHASH_Int(string key, string val)
    {
        var data = this.DB.HashGet(key, val);
        int Idata = 0;
        if (data.HasValue == true)
        {
            int DataSize = data.ToString().Length - 1;
            int sizenum = (int)Math.Pow(10, DataSize);
            foreach (var d in data.ToString())
            {
                if ('0' <= d && d <= '9')
                {
                    Idata += ((d - 48) * sizenum);
                    sizenum /= 10;
                }
                else
                {
                    return -int.MaxValue;
                }
            }
        }
        return Idata;
    }
    public double GetHASH_Double(string key, string val)
    {
        var data = this.DB.HashGet(key, val);
        if (data.HasValue == true)
        {
            double Idata = Double.Parse(data);
            return Idata;
        }
        return -Double.MaxValue;
    }
    public void SetHASH(string key, HashEntry[] val)
    {
        this.DB.HashSet(key, val);
    }
    public void DestroyHash(string key, RedisValue field)
    {
        this.DB.HashDelete(key, field);
    }

또한 Redis 서버에 메시지를 따로보내는 방법이 있는데 

그게 아까 보여주었던 sub 로 가능하다

    public void Meseage(string Channel, string Meseage)
    {
        sub.Publish(Channel, Meseage);
    }

특정 채널에 메시지를 보낼수있다.

또한 채널의 메시지를 구독하여 올라올때마다 확인받을수 있다.

 

    public void Setsubscrib(string _channel, CallBack_Funtc _CallBack_Funtc)
    {
        if (redisConnection.IsConnected)
        {
            sub.Subscribe(_channel, (channel, message) =>
            {
                try
                {
                    _CallBack_Funtc?.Invoke(channel+" "+message);
                    Debug.Log(message);
                }
                catch
                {

                }
            });
        }
    }

 

 

 

 

 

 

위에서 만들었던 클래스 사용

  private string Key;
    public string RedisIP_Num;
    public int PortNum;
    public bool GetDataLoop;
    public int ThreadDelay;

    private void Start()
    {
     // 따로 XMl로 받아오는 데이터들
        Key = Datas.data.DataKey;
        RedisIP_Num = Datas.data.RedisIP_Num;
        PortNum = Datas.data.PortNum;
        GetDataLoop = Datas.data.GetDataLoop;
        ThreadDelay = Datas.data.ThreadDelay;

        redis = new Redis();
        if (redis.Init(RedisIP_Num, PortNum, delegate
         {
             while (GetDataLoop)
             {
                 GEtD(); // redis.GetHASH_Bool(Key, "KeyData") 데이터들을 받아오는 함수
                 Thread.Sleep(ThreadDelay);

             }
         }))
        {
            Debug.Log("connect");
        }
        else
        {
            Debug.Log("Non_connect");
        }

 GEtD()함수 내부 코드는 이렇게 데이터를 받는다.

 

또한 Init 의 델리게이트에 데이터를 받아오는 함수와 스레드.Sleep(변수)

를 이용하여 연결이 완료되었을때 데이터를 주기적으로 받아오게 만들었다.

 

 

 

전체코드

더보기

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using StackExchange.Redis;
using System.Threading;
using System;
using System.Threading.Tasks;

namespace TStory
{
    static public class Global
    {
        public static bool Dnet_Connection;
        public static bool Dnet_readwrite;
        public static bool Dnet_error;
        public static double server_cache_alive;
        public static int client_connection_count;

    }
}

public class RDS : MonoBehaviour
{
    private Redis redis;
    public RedisValue redisValue;

    private string Key;
    public string RedisIP_Num;
    public int PortNum;
    public bool GetDataLoop;
    public int ThreadDelay;

    private void Start()
    {
        Key = Datas.data.DataKey;
        RedisIP_Num = Datas.data.RedisIP_Num;
        PortNum = Datas.data.PortNum;
        GetDataLoop = Datas.data.GetDataLoop;
        ThreadDelay = Datas.data.ThreadDelay;

        redis = new Redis();
        if (redis.Init(RedisIP_Num, PortNum, delegate
        {
            while (GetDataLoop)
            {
                GEtD();
                Thread.Sleep(ThreadDelay);

            }
        }))
        {
            Debug.Log("connect");
        }
        else
        {
            Debug.Log("Non_connect");
        }


    }

    private void OnApplicationQuit()
    {
        GetDataLoop = false;
        //redis.T.Join();
    }

    public void GEtD()
    {
        TStory.Global.Dnet_Connection = (redis.GetHASH_Bool(Key, "TStory-connection"));
        TStory.Global.Dnet_readwrite = (redis.GetHASH_Bool(Key, "TStory-readwrite"));
        TStory.Global.Dnet_error = (redis.GetHASH_Bool(Key, "device-net-error"));
        TStory.Global.server_cache_alive = (redis.GetHASH_Double(Key, "server-cache-alive"));
        TStory.Global.client_connection_count = (redis.GetHASH_Int(Key, "client-connection-count"));

    }

}

public class Redis
{
    private ConnectionMultiplexer redisConnection;
    private IDatabase DB;
    private ISubscriber sub;
    public delegate void Events();
    public Thread T;
    public bool Init(string host, int port)
    {

        this.redisConnection = ConnectionMultiplexer.Connect(host + ":" + port);
        if (redisConnection.IsConnected)
        {
            this.DB = this.redisConnection.GetDatabase();
            sub = redisConnection.GetSubscriber();
            return true;
        }

        return false;
    }

    public bool Init(string host, int port, Events LoopEvents)
    {


        T = new Thread(new ThreadStart(LoopEvents));
        try
        {
            ConfigurationOptions option = new ConfigurationOptions
            {
                ConnectTimeout = 1000,
                AbortOnConnectFail = false,
                EndPoints = { $"{host}:{port}" },
            };
            redisConnection = ConnectionMultiplexer.Connect(option);
        }
        catch (Exception e)
        {
            Debug.Log(e);
        }

        if (redisConnection.IsConnected)
        {
            this.DB = this.redisConnection.GetDatabase();
            sub = redisConnection.GetSubscriber();
            T.Start();

            return true;
        }

        return false;
    }
    public string GetString(string key)
    {
        return this.DB.StringGet(key);
    }
    public bool SetString(string key, string val)
    {
        return this.DB.StringSet(key, val);
    }
    public RedisValue GetHASH(string key, string val)
    {
        return this.DB.HashGet(key, val);
    }
    public bool GetHASH_Bool(string key, string val)
    {
        var data = this.DB.HashGet(key, val);
        if (data.ToString()[0] == 't' || data.ToString()[0] == 'T')
        {
            return true;
        }
        return false;
    }
    public int GetHASH_Int(string key, string val)
    {
        var data = this.DB.HashGet(key, val);
        int Idata = 0;
        if (data.HasValue == true)
        {
            int DataSize = data.ToString().Length - 1;
            int sizenum = (int)Math.Pow(10, DataSize);
            foreach (var d in data.ToString())
            {
                if ('0' <= d && d <= '9')
                {
                    Idata += ((d - 48) * sizenum);
                    sizenum /= 10;
                }
                else
                {
                    return -int.MaxValue;
                }
            }
        }
        return Idata;
    }
    public double GetHASH_Double(string key, string val)
    {
        var data = this.DB.HashGet(key, val);
        if (data.HasValue == true)
        {
            double Idata = Double.Parse(data);
            return Idata;
        }
        return -Double.MaxValue;
    }
    public void SetHASH(string key, HashEntry[] val)
    {
        this.DB.HashSet(key, val);
    }
    public void DestroyHash(string key, RedisValue field)
    {
        this.DB.HashDelete(key, field);
    }
    public void Meseage(string Channel, string Meseage)
    {
        sub.Publish(Channel, Meseage);
    }

}

 

'엔진 > 유니티' 카테고리의 다른 글

Unity Spline 기능 추가!  (0) 2023.06.01
Unity Simulator  (0) 2023.01.20
Unity ObjectPool Api  (0) 2022.11.02
unity _Prefab Variants  (0) 2022.03.17
Unity Inspector 창 정리  (2) 2022.02.24
반응형

n Nulnud-coalescing operator  

   NULL 병합 연산자

   ??  , ??=

 

특징  : 오른쪽 결합

 

 

null 병합 연산자 라고 부르는 ?? 연산자는

null이 아닌 경우 왼쪽 값을 반환하고 null인 경우 오른쪽 값을 확인 하고 반환하는 연산자다.

만약 왼쪽값이 null 이 아닌경우  오른쪽 값을 확인하지 않는다.

 

 

            int? a = 17;
            int b = 15;
            int? c = null;
            
            Console.WriteLine(a ?? b); // Output :17,  왼쪽값이 null이 아니기때문에 17.
            Console.WriteLine(c ?? b); //Output :15,   왼쪽(null)->오른쪽 값을 확인해 본뒤 b반환
            Console.WriteLine(c ?? c); //Output : ,    둘다 null 이기 때문에 null
            //Console.WriteLine(b ?? c); Err ?? 연산자는 int 및 int? 형식의 피연산자에 적용할수 없다

int? 가 뭔지 모르겠다면 이 블로그의 Nullable 을 보고오는게 좋다.

https://programing-note.tistory.com/entry/c-Nullabe-%EA%B0%92-%ED%98%95%EC%8B%9D

 

 

c#8.0 이상에서 사용할수 있는null 병합 할당 연산자(??=)는  null 병합 연산자 와 비슷하다. 

  왼쪽 값이 null인경우 우측 값을 할당시켜준다. 물론 좌측값이 null이 아닌경우 우측값을 확인하지 않는다.

            int? a = null;
            int b = 15;
            a ??= b; 좌측값이 null 이기때문에 우측b 값을 확인후 a값에 대입한다.
            Console.WriteLine(a); // 15

 이 연산자는 가끔 유용하게 쓰일수있다.

 

 

예시로는 null인지 확인하는 구문을 간단하게 만들수있다.

            if (isNull is null)
            {
                isNull = 10;
            }

이러한 연산을

            isNull ??= 10;

이렇게 간단하게 쓸수 있다.

 

 

 

Null 조건부 연산자 ?. 및 ?[]

C# 6이상부터 사용할수 있는 연산자다.

이는 Null이 아닌것으로 평가되었을 때만 멤버 엑세스 ?. ,또는 요소 엑세스?[], 연산을 피연산자 에게 적용하고

 Null 인것으로 평가되었을경우 Null을 반환한다.

 

  조건부 멤버나 요소 액세스 작업에서 null 을 반환하면 나머지 체인은 실행되지 않는다.

 

A?.B?.c

위 와 같을때 A가 Null일때 B는 평가되지 않는다.

이와 같이 A또는 B가 Null일경우 C는 평가되지 않는다.

 

아래 윈도우 예제 코드를 보면 쉽게 이해할수 있다.

            
            double SumNumbers(List<double[]> setsOfNumbers, int indexOfSetToSum)
            {
                return setsOfNumbers?[indexOfSetToSum]?.Sum() ?? double.NaN;
            }

            var sum1 = SumNumbers(null, 0);
            Console.WriteLine(sum1);  // output: NaN
            var numberSets = new List<double[]>
                 {
                     new[] { 1.0, 2.0, 3.0 },
                     null
                  };

            var sum2 = SumNumbers(numberSets, 0);
            Console.WriteLine(sum2);  // output: 6

            var sum3 = SumNumbers(numberSets, 1);
            Console.WriteLine(sum3);  // output: NaN
            
                //var sum4 = SumNumbers(numberSets, 2); 예외발생

위 코드처럼 코드를 짤때 Null 이 들어가 오류가 생기는 상황을 간단하게 처리할수있다.

 

 

주의 할점

a.x 또는 a[x] 가 예외를 던지면 a?.x 또는 a?[x] 는 null이 아닌 예외를 던진다.

위의 예제 마지막줄 처럼 x가 a의 배열의 크기 보다 큰경우 a?[x] 는 IndexOutOFRangeExcption 예외를 던진다.

 

참고

?. 연산자는 왼쪽 연산자를 한번만 계산하고 null이 아닌것을 확인한 후에는 null로 변경할수 없도록 보장한다.

 

또한 스레드를 사용할때도 안전하게 호출할수 있다.

스레드를 호출할때 null인지 확인하고 호출하는 것을

var handler = this.PropertyChanged;
if (handler != null)
{
    handler(…);
}
 
 를 아래처럼 변경할수있다

PropertyChanged?.Invoke(…)

위 상황처럼 간단하게  사용할수있다

 

참고 링크 :docs.microsoft.com;

'언어 > C#' 카테고리의 다른 글

c# Nullable값 형식  (0) 2022.03.24
c# Tuple  (0) 2022.03.23
C# Ref,Out 키워드 와 in 키워드  (0) 2022.03.16
반응형

Nullable 값 형식 : 기본 값 형식의 모든 값과 추가 null 값을 나타낼수 있다.

 

예시로 

 bool 변수는 true false 두가지를 나타내는게 가능하고 Null은 표현불가능하다.

하지만 null 이 필요할수 있기때문에  자료형 뒤에 ? 를 붙이면 null 표현이 가능해진다.

 

사용 방식 

값 타입 자료형 ? 변수명

    int? a = 10;
    int? b = null;
   // int c = null;  null을 허용하지 않는 값 형식
   
   class A{}
  // A a?; null 을 허용하지 않는  값 형식 이여야 한다.
  
  int?[] arr = new int?[10];
  //배열 선언방법

 

 

Nullable 값 형식에서 기본 형식으로 변환 

   int? a = 10;
   int b = 0;
   b = a ?? -1;
   // a가 null 이면 -1이 들어가고  null이 아닐경우 a값이 그대로 들어가 10이 들어간다.

 

?? 연산자는 왼쪽 값이 null 이면  오른쪽 값을 사용하고 null이 아닐경우 왼쪽 값을 사용하는 

일종의 null 을 검사하는 연산자다. 

 

 

 

 

 

is

is 연산자로는 Nullable 형식인지 아닌지 구분 불가하다

마소 예제코드

int? a = 1;
int  b = 2;

if ( a is int )
{
  Console.WriteLine(int? instance is compatible with int);
}
if (b is int?)
{
    Console.WriteLine("int instance is compatible with int?");
}

// Output:
// int? instance is compatible with int
// int instance is compatible with int?

 

 

 

GetType 

Object.GetType은 Null 허용값 형식의 boxing으로 기본형식 값의 boxing과 동일하게 나온다.

GetType은 기본형식 Type 인스턴스를 반환한다.

int? a = 17;
Type typeOfA = a.GetType();
Console.WriteLine(typeOfA.FullName);
// Output:
// System.Int32

 

그럼으로 아래 코드 처럼 구분할수 있다

마소코드

Console.WriteLine($"int? is {(IsNullable(typeof(int?)) ? "nullable" : "non nullable")} value type");
Console.WriteLine($"int is {(IsNullable(typeof(int)) ? "nullable" : "non-nullable")} value type");

bool IsNullable(Type type) => Nullable.GetUnderlyingType(type) != null;

// Output:
// int? is nullable value type
// int is non-nullable value type

 

 

 

참고 링크 :docs.microsoft.com;

'언어 > C#' 카테고리의 다른 글

c# ?? 연산자 (Null 병합 연산자)  (0) 2022.07.12
c# Tuple  (0) 2022.03.23
C# Ref,Out 키워드 와 in 키워드  (0) 2022.03.16
반응형

c# 튜플 : 간단한 데이터 구조로 여러 데이터 요소를 그룹화 할수있다.

 

튜플 변수 선언

(원하는 자료형 , 원하는 자료형 , ... ) 변수명;

// 선언과 동시에 초기화
(double,int) t = ( 1.1 , 3 );
 t.Item1 = 10;

(string name , int age) t2;
t2 = (name: "Mok", age: 5);
t2.age = 20;

이런식으로 간단하게 그룹화 할수있다

또한 튜플은 함수의 반환형식 에서도 사용할수 있다.

 

 

튜플 함수 사용

(원하는 자료형 , 원하는 자료형 , ... ) 함수명 (매개변수s..) { 내부구현...  retrun(반환값,...) };

  
   (int min, int max) FindMinMax(int[] input)
        {
         //... 내부구현
            return (min, max);
        }

위 함수처럼 자료형 클래스 말고도 묶어서 사용이 가능하다.

 

 

c# 에서 제공한 튜플의 사용 사례를 보면 더욱 쉽게 알수있다.

       //마소 c# 튜플 사용 사례
       var ys = new[] { -9, 0, 67, 100 };
        var (minimum, maximum) = FindMinMax(ys);
        // Output:
        // Limits of [-9 0 67 100] are -9 and 100

        (int min, int max) FindMinMax(int[] input)
        {
            if (input is null || input.Length == 0)
            {
            }

            var min = int.MaxValue;
            var max = int.MinValue;
            foreach (var i in input)
            {
                if (i < min)
                {
                    min = i;
                }
                if (i > max)
                {
                    max = i;
                }
            }
            return (min, max);
        }

위 사용 사례에서 보이는것 처럼 minimum 과 maximum 을 따로 구하지 않고 한번의 묶음으로 처리하여

하나의 함수로 처리하는것을 볼수있다.

 

 

또다른 사용 예시로는 비교문에서 알수있다.

//튜플 비교문


(int a, byte b) left = (5, 10);
(long a, int b) right = (5, 10);
Console.WriteLine(left == right);  // output: True
Console.WriteLine(left != right);  // output: False

var t1 = (A: 5, B: 10);
var t2 = (B: 5, A: 10);
Console.WriteLine(t1 == t2);  // output: True
Console.WriteLine(t1 != t2);  // output: False

보이는것 처럼 두개의 묶음으로 비교하여 비교문을 줄일수 있다.

 

 

튜플은  Dictionary 에서도 사용이 가능하다.

var limitsLookup = new Dictionary<int, (int Min, int Max)>()
{
    [2] = (4, 10),
    [4] = (10, 20),
    [6] = (0, 23)
};

if (limitsLookup.TryGetValue(4, out (int Min, int Max) limits))
{
    Console.WriteLine($"Found limits: min is {limits.Min}, max is {limits.Max}");
}
// Output:
// Found limits: min is 10, max is 20

이처럼 쉽게 저장하고 사용하는것을 볼수있다.

 

 

참고

 1. 지금 사용한 튜플 형식사용은 기존 Tuple class 와는 다르다.

    ㄴ 지금 사용한 형식은 System.ValueTuple 이고 Tuple class는 System.Tuple 이다.

 2.  System.ValueTuple 형식은 데이터 멤버는 필드이다.

 2.1  System.Tuple 형식은 데이터 멤버는 속성이다.

 

3. anonymous type 과 비교도 가능하다.

var apple = new { Item = "apples", Price = 1.35 };
(string,double) appe = ( "apples", 1.35 );

if(appe == appe)
{
Console.WriteLine("true"); //true
}

 

 

익명 타입, 튜플, vlaueTuple 들의 주요 차이점은 다음과 같다

이름 액세스 한정자 형식 사용자 지정 멤버 이름 분해 지원 식트리 지원
anonymous type internal o x o
Tuple public x x o
ValueTuple public o o x

 

참고자료 : docs.microsoft.com

'언어 > C#' 카테고리의 다른 글

c# ?? 연산자 (Null 병합 연산자)  (0) 2022.07.12
c# Nullable값 형식  (0) 2022.03.24
C# Ref,Out 키워드 와 in 키워드  (0) 2022.03.16
반응형

 

 

 

2018버전부터 Prefab Variants 라는것을 지원한다.

Prefab Variants 는 프리펩의 자식 계념이라 생각하면 쉽다.

 

왼쪽이 Variants  ,오른쪽이 Original

 

아래 그림처럼 기존 프리팹을 바꿔서 Original 프리팹을 재생성 하게되었을때 

 

 

 

 

 

 

 

 

두개로 나뉘어버린 프리팹에 같은걸 추가하고 싶을땐

둘다 다시작업을 해주는 경우가 생긴다

이런경우 Variants 프리팹을 사용하면

 

 

아래 사진처럼 부모 프리팹을 수정하면 자식 프리팹도 수정이 된다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

왼쪽이 기존 프리팹 예시이고 오른쪽이 variants 예시다.

프리팹을 새로 만든후, 붉은 네모를 원본에 넣었을때 아래 예시처럼 작동한다.

 

 

 

 

 

 

 

 

 

 

 

 

모두 붉은 박스를 넣어주려고 할때

왼쪽이미지 처럼 복사 하여 생성한 프리팹은 붉은 박스를 전부 넣어주는 시간이 들어간다.

하지만 프리팹을 부모로 두고 variants로 작업할경우 쉽게 변형 시키고 추가 할수 있다.

 

 

 

 

 

 

variant 생성방법은

 

아래 사진처럼     Original 프리팹 우클릭->Create->Prefab Variant 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

또는  Hierarchy 창에서 오리지널 프리팹을 Project 창으로 드래그시 

 어떤식으로 저장할지 Create Prefab 창이 뜬다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

참고한 링크 :https://www.youtube.com/watch?v=pv30sE_Vsis

'엔진 > 유니티' 카테고리의 다른 글

Unity Spline 기능 추가!  (0) 2023.06.01
Unity Simulator  (0) 2023.01.20
Unity ObjectPool Api  (0) 2022.11.02
unity Redis  (0) 2022.07.13
Unity Inspector 창 정리  (2) 2022.02.24
반응형

 

Ref 키워드와 Out 키워드를 더 잘 이해하기 위해서는 call by refurence , call by value를 알고 보는 게 좋다.

Ref , Out 키워드

정의부터 설명하자면

 Ref Pass by Reference

   ㄴ 얕은 복사 매개변수 지정자

Out Output Parameters

   ㄴ 출력용 매개변수 - 내부에서 값을 할당해주어야 한다.

 

 

사용 방법

 ref out 사용법

 

함수 선언 시

 리턴 값 함수명 (ref & out키워드 매개변수 타입  변수 이름 ) <-함수를 선언 또는 구현 시에도 매개변수 앞에 써줘야 한다.

함수 사용 시

 함수명( ref & out 키워드 변수 ) <- 사용할 때에도 매개변수 앞에다 ref 키워드를 써줘야 한다.

    public void FuncRefB(ref int A) 
    {
        A++;
    }

    public void Func( int A)
    {
        A++;
    }

    public void FuncOutB(out int A)
    {
        A = 10; // 내부에서 값을 대입 하지 않으면 오류가 나온다.
    }
   
       ...
       {
        int A;
        
        FuncOutB(out A);
          Console.WriteLine(A); // 내부에서 생성해서 받는다 Call by Refurence + @  10
        Func(A);
          Console.WriteLine(A); // 늘어나지 않는다 Call by Value   10
        FuncRefB(ref A);
          Console.WriteLine(A); // 값이 늘어난다. Call by Refurence  11
        }

이처럼 c나 c++처럼 포인터 참조형처럼 Call by Reference 형식으로 넘겨줄 수 있다.

 

(참고할 점)

1. Ref 키워드는 할당이 되어있어야 한다.

2. Out 은 Ref처럼 할당이 되어있지 않아도 사용 가능하지만. 내부에서 새로 할당한다.(이전 값은 사라짐)

3. Ref int -> int 오버로드는 가능하나 Out -> Ref , in 오버로드는 불가능하다.

 

 

구조체 같은 경우 기본적으로 call by value 형식이기 때문에 구조체를 ref 키워드로 넘겨주면 
얕은 복사(주소 값)처럼 사용할 수 있다.

 

in 키워드

in : 읽기 전용 매개변수 지정자

 

함수 선언 시

 리턴 값 함수명 ( in 키워드 매개변수 타입  변수이름 ) <-함수를 선언 또는 구현 시에도 매개변수 앞에 써줘야 한다.

함수 사용 시

 함수명(  키워드 변수 ) <-  in은 사용할 때 앞에 붙여 주지 않는다.

 

void FuncIn(in int num)
{
  //num++; 내부 수정불가능
  
}

내부에서 수정이 불가능하기 때문에 혹여나 다른 값을 입력하거나 수정하는 실수를 미연에 방지할 수 있다.

 

 

Ref 와 in 키워드는  복사 비용을 참조로 변경하여 성능 향상을 생각할수 있다.

 

 

--추가--

 

out 키워드 의경우 C#7.0 버전부터는 튜블 반환을 지원했다.

튜플 반환이 있기전에 여러값을 반환 할수 없었기 때문에 out을 주로 사용하였다.

 

(int sum, int product) Calculate(int a, int b) => (a + b, a * b);

 

 

void Calculate(int a, int b, out int sum, out int product)
{
    sum = a + b;
    product = a * b;
}

Calculate(2, 3, out int sum, out int product);

 

out 키워드는 성능이 중요한 코드나 오래된 api, 호환성 때문에 쓰인다.

 

 

 

반복적으로 많이 쓰일때 생성되는것보다 out으로 불필요한 메모리할당과 GC부하를 줄일수있다.

 

 

 

 

 

 

 

 

 

 

 

 

 

'언어 > C#' 카테고리의 다른 글

c# ?? 연산자 (Null 병합 연산자)  (0) 2022.07.12
c# Nullable값 형식  (0) 2022.03.24
c# Tuple  (0) 2022.03.23

+ Recent posts