using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApp2
{
public enum ClassType
{
Knight,
Archer,
Mage
}
public class Player
{
public ClassType ClassType { get; set; }
public int Level { get; set; }
public int HP { get; set; }
public int Attack { get; set; }
public List<int> Items { get; set; } = new List<int>();
}
class Linq
{
static List<Player> _players = new List<Player>();
static void Main(string[] args)
{
Random rand = new Random();
for(int i=0;i<100;++i)
{
ClassType type = ClassType.Knight;
switch(rand.Next(0, 3))
{
case 0:
type = ClassType.Knight;
break;
case 1:
type = ClassType.Archer;
break;
case 2:
type = ClassType.Mage;
break;
}
Player player = new Player()
{
ClassType = type,
Level = rand.Next(1, 100),
HP = rand.Next(100, 1000),
Attack = rand.Next(5, 50)
};
for(int j=0;j<5;++j)
{
player.Items.Add(rand.Next(1, 101));
}
_players.Add(player);
}
//join
{
List<int> levels = new List<int>() { 1, 5, 9};
var playerLevels =
from p in _players
join l in levels //player와 levels를 조인하는데
on p.Level equals l //레벨 i 와 player.level 이 같은것만 조인한다
select p;
foreach(var p in playerLevels)
{
Console.WriteLine(p.Level);
}
int test = 0;
}
GetHightLevelKnights();
}
//레벨이 50 이상인 knight 만 추려서 레벨을 낮음->놎음 순으로 정렬
private static void GetHightLevelKnights()
{
//linq 문법으로 db 쿼리문 처럼 조회 할 수 있다
//from 은 foreach 로 생각해도 괜찮다
//실행 순서는 from where orderby select 순으로실행된다 생각하면 된다
var players =
from p in _players
where p.ClassType == ClassType.Knight && p.Level >= 50
orderby p.Level
select p;
foreach (Player p in players)
{
Console.WriteLine($"{ p.Level} {p.HP} {p.ClassType}");
}
}
}
}
player와 levels를 조인하는데 레벨 i 와 player.level 이 같은것만 조인하게 한다
List<int> levels = new List<int>() { 1, 5, 9};
var playerLevels =
from p in _players
join l in levels
on p.Level equals l
select p;
표준 Linq 방식
public enum ClassType
{
Knight,
Archer,
Mage
}
public class Player
{
public ClassType ClassType { get; set; }
public int Level { get; set; }
public int HP { get; set; }
public int Attack { get; set; }
public List<int> Items { get; set; } = new List<int>();
}
class Linq
{
static List<Player> _players = new List<Player>();
static void Main(string[] args)
{
Random rand = new Random();
for(int i=0;i<100;++i)
{
ClassType type = ClassType.Knight;
switch(rand.Next(0, 3))
{
case 0:
type = ClassType.Knight;
break;
case 1:
type = ClassType.Archer;
break;
case 2:
type = ClassType.Mage;
break;
}
Player player = new Player()
{
ClassType = type,
Level = rand.Next(1, 100),
HP = rand.Next(100, 1000),
Attack = rand.Next(5, 50)
};
for(int j=0;j<5;++j)
{
player.Items.Add(rand.Next(1, 101));
}
_players.Add(player);
}
//중첩 from , ex : 모든 아이템 목록을 추출할때
{
var items = from p in _players
from i in p.Items
where i > 95
select new { p, i };
var li = items.ToList();
foreach(var elem in li)
{
Console.WriteLine(elem.i +" : " + elem.p);
}
}
//linq 표준연산자
{
var players =
from p in _players
where p.ClassType == ClassType.Knight && p.Level >= 50
orderby p.Level
select p;
//위 결과와 아래 결과는 같다
//from 은 생략 가능
var sameResult = _players
.Where(p => p.ClassType == ClassType.Knight && p.Level >= 50)
.OrderBy(p => p.Level)
.Select(p => p);
int iii = 0;
}
두개의 결과가 같은 것을 볼 수 있다
//linq 표준연산자
{
var players =
from p in _players
where p.ClassType == ClassType.Knight && p.Level >= 50
orderby p.Level
select p;
//위 결과와 아래 결과는 같다
//from 은 생략 가능
var sameResult = _players
.Where(p => p.ClassType == ClassType.Knight && p.Level >= 50)
.OrderBy(p => p.Level)
.Select(p => p);
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApp2
{
public enum ClassType
{
Knight,
Archer,
Mage
}
public class Player
{
public ClassType ClassType { get; set; }
public int Level { get; set; }
public int HP { get; set; }
public int Attack { get; set; }
public List<int> Items { get; set; } = new List<int>();
}
class Linq
{
static List<Player> _players = new List<Player>();
static void Main(string[] args)
{
Random rand = new Random();
for(int i=0;i<100;++i)
{
ClassType type = ClassType.Knight;
switch(rand.Next(0, 3))
{
case 0:
type = ClassType.Knight;
break;
case 1:
type = ClassType.Archer;
break;
case 2:
type = ClassType.Mage;
break;
}
Player player = new Player()
{
ClassType = type,
Level = rand.Next(1, 100),
HP = rand.Next(100, 1000),
Attack = rand.Next(5, 50)
};
for(int j=0;j<5;++j)
{
player.Items.Add(rand.Next(1, 101));
}
_players.Add(player);
}
//중첩 from , ex : 모든 아이템 목록을 추출할때
{
var items = from p in _players
from i in p.Items
where i > 95
select new { p, i };
var li = items.ToList();
foreach(var elem in li)
{
Console.WriteLine(elem.i +" : " + elem.p);
}
}
GetHightLevelKnights();
}
//레벨이 50 이상인 knight 만 추려서 레벨을 낮음->놎음 순으로 정렬
private static void GetHightLevelKnights()
{
//linq 문법으로 db 쿼리문 처럼 조회 할 수 있다
//from 은 foreach 로 생각해도 괜찮다
//실행 순서는 from where orderby select 순으로실행된다 생각하면 된다
var players =
from p in _players
where p.ClassType == ClassType.Knight && p.Level >= 50
orderby p.Level
select p;
foreach (Player p in players)
{
Console.WriteLine($"{ p.Level} {p.HP} {p.ClassType}");
}
}
}
}
palyer 에
public List<int> Items { get; set; } = new List<int>();
를 추가 한다음
from from 구문으로 item 의 모든 목록을 출력하는 구문이다
//중첩 from , ex : 모든 아이템 목록을 추출할때
{
var items = from p in _players
from i in p.Items
where i > 95
select new { p, i };
var li = items.ToList();
foreach(var elem in li)
{
Console.WriteLine(elem.i +" : " + elem.p);
}
}
Group 처리
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApp2
{
public enum ClassType
{
Knight,
Archer,
Mage
}
public class Player
{
public ClassType ClassType { get; set; }
public int Level { get; set; }
public int HP { get; set; }
public int Attack { get; set; }
public List<int> Items { get; set; } = new List<int>();
}
class Linq
{
static List<Player> _players = new List<Player>();
static void Main(string[] args)
{
Random rand = new Random();
for(int i=0;i<100;++i)
{
ClassType type = ClassType.Knight;
switch(rand.Next(0, 3))
{
case 0:
type = ClassType.Knight;
break;
case 1:
type = ClassType.Archer;
break;
case 2:
type = ClassType.Mage;
break;
}
Player player = new Player()
{
ClassType = type,
Level = rand.Next(1, 100),
HP = rand.Next(100, 1000),
Attack = rand.Next(5, 50)
};
for(int j=0;j<5;++j)
{
player.Items.Add(rand.Next(1, 101));
}
_players.Add(player);
}
//중첩 from , ex : 모든 아이템 목록을 추출할때
{
var items = from p in _players
from i in p.Items
where i > 95
select new { p, i };
var li = items.ToList();
foreach(var elem in li)
{
Console.WriteLine(elem.i +" : " + elem.p);
}
}
//group
{
var playerByLevel = from p in _players
group p by p.Level into g //g 로 그룹화된 데이터를 받는다
orderby g.Key
select new { g.Key, Players = g };
int jj = 0;
}
GetHightLevelKnights();
}
//레벨이 50 이상인 knight 만 추려서 레벨을 낮음->놎음 순으로 정렬
private static void GetHightLevelKnights()
{
//linq 문법으로 db 쿼리문 처럼 조회 할 수 있다
//from 은 foreach 로 생각해도 괜찮다
//실행 순서는 from where orderby select 순으로실행된다 생각하면 된다
var players =
from p in _players
where p.ClassType == ClassType.Knight && p.Level >= 50
orderby p.Level
select p;
foreach (Player p in players)
{
Console.WriteLine($"{ p.Level} {p.HP} {p.ClassType}");
}
}
}
}
코드 중
//group
{
var playerByLevel = from p in _players
group p by p.Level into g //g 로 그룹화된 데이터를 받는다
orderby g.Key
select new { g.Key, Players = g };
}
이 부분이 플레이어들을 같은 level 로 그룹화 한다음 각 그룹끼리는 key 값에 의해 정렬 되도록 처리한 것이다
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApp2
{
public enum ClassType
{
Knight,
Archer,
Mage
}
public class Player
{
public ClassType ClassType { get; set; }
public int Level { get; set; }
public int HP { get; set; }
public int Attack { get; set; }
}
class Linq
{
static List<Player> _players = new List<Player>();
static void Main(string[] args)
{
Random rand = new Random();
for(int i=0;i<100;++i)
{
ClassType type = ClassType.Knight;
switch(rand.Next(0, 3))
{
case 0:
type = ClassType.Knight;
break;
case 1:
type = ClassType.Archer;
break;
case 2:
type = ClassType.Mage;
break;
}
Player player = new Player()
{
ClassType = type,
Level = rand.Next(1, 100),
HP = rand.Next(100, 1000),
Attack = rand.Next(5, 50)
};
_players.Add(player);
}
GetHightLevelKnights();
}
//레벨이 50 이상인 knight 만 추려서 레벨을 낮음->놎음 순으로 정렬
private static void GetHightLevelKnights()
{
//linq 문법으로 db 쿼리문 처럼 조회 할 수 있다
//from 은 foreach 로 생각해도 괜찮다
//실행 순서는 from where orderby select 순으로실행된다 생각하면 된다
var players =
from p in _players
where p.ClassType == ClassType.Knight && p.Level >= 50
orderby p.Level
select p;
foreach (Player p in players)
{
Console.WriteLine($"{ p.Level} {p.HP} {p.ClassType}");
}
}
}
}
linq 문법으로 db 쿼리문 처럼 조회 할 수 있다 from 은 foreach 로 생각해도 괜찮다 실행 순서는 from where orderby select 순으로실행된다 생각하면 된다
=> 이것이 원래 db 의 쿼리 구문을 이해하는 순서인데 c# 의 linq 에서는 select를 아예 밑으로 내려놨음
var players = from p in _players where p.ClassType == ClassType.Knight && p.Level >= 50 orderby p.Level select p;
유니티에선 linq 가 ios 버전에서 문제가 발생 할 수 있어서(불안전해서) 안쓰는게 좋다
using System;
using System.Threading.Tasks;
namespace ConsoleApp2
{
class Program
{
//async/await : 비동기인데 멀티스레드가 아닌 경우가 있다 : coroutine 와 유사하다
//task 는 일감을 말함
static Task test()
{
Console.WriteLine("start test");
//3초 후에 task 를 반환한다
Task t = Task.Delay(3000);
return t;
}
static void Main(string[] args)
{
//위에서 task 를 만듬과 동시에 시간은 흐르게 된다
Task t = test();
Console.WriteLine("while start");
while(true)
{
}
}
}
}
async/await : 비동기인데 멀티스레드가 아닌 경우가 있다 : coroutine 와 유사하다
Task 를 생성하고 delay 에 시간을 넣으면 생성과 동시에 시간이 흘러간다는 것을 알 수 있다
start test 문자가 뜨고 시간 지연 없이 바로 while start 가 호출되는데
static Task test()
{
Console.WriteLine("start test");
//3초 후에 task 를 반환한다
Task t = Task.Delay(3000);
return t;
}
static void Main(string[] args)
{
//위에서 task 를 만듬과 동시에 시간은 흐르게 된다
Task t = test();
//처리 지점
t.Wait();
Console.WriteLine("while start");
while(true)
{
}
}
wait 이 들어가면 start test 가 찍힌 후 3초 이후에 while start 가 호출 되는것을 알 수 있다
그래서 //처리 지점 에서 task 가 완료 되는 동안 다른 동작을 할 수 있게 된다
using System;
using System.Threading.Tasks;
namespace ConsoleApp2
{
class Program
{
static async void testAsync()
{
Console.WriteLine("start testAsync");
//3초 후에 task 를 반환한다
Task t = Task.Delay(3000);
await t;
Console.WriteLine("end testAsync");
}
static void Main(string[] args)
{
//위에서 task 를 만듬과 동시에 시간은 흐르게 된다
testAsync();
Console.WriteLine("while start");
while(true)
{
}
}
}
}
이 코드는 async/await 인데 결과는 다음과 같다
async 함수가 호출되고 task 가 완료 될때까지 await 으로 기다려지긴 하지만 제어권이 main 으로 넘어가서 처리할 것을 먼저 처리 할수 있는 형태가 된다 typescript 에 이와 유사한 문법들이 있다
또는 다음 처럼 간략한 버전으로 사용 할 수도 있다
using System;
using System.Threading.Tasks;
namespace ConsoleApp2
{
class Program
{
static async void testAsync()
{
Console.WriteLine("start testAsync");
//3초 후에 task 를 반환한다
await Task.Delay(3000);
Console.WriteLine("end testAsync");
}
static void Main(string[] args)
{
//위에서 task 를 만듬과 동시에 시간은 흐르게 된다
testAsync();
Console.WriteLine("while start");
while(true)
{
}
}
}
}
main 에서 testAsync 함수가 완료 될때까지 대기 시킬 수도 있다 다음 처럼 대기 할수 있고
using System;
using System.Threading.Tasks;
namespace ConsoleApp2
{
class Program
{
static async Task testAsync()
{
Console.WriteLine("start testAsync");
//3초 후에 task 를 반환한다
await Task.Delay(3000);
Console.WriteLine("end testAsync");
}
static async Task Main(string[] args)
{
//위에서 task 를 만듬과 동시에 시간은 흐르게 된다
await testAsync();
Console.WriteLine("while start");
while(true)
{
}
}
}
}
testAsync 함수가 모두 완료 될때까지 awiat testAsync(); 구문에서 대기하다가 완료 된 이후 main 의 나머지 구문을 이어 나간다
결과는 다음과 같다
값을 리턴 받고 싶다면 Task<T> 를 리턴하면 된다
using System;
using System.Threading.Tasks;
namespace ConsoleApp2
{
class Program
{
static async Task<int> testAsync()
{
Console.WriteLine("start testAsync");
//3초 후에 task 를 반환한다
await Task.Delay(3000);
Console.WriteLine("end testAsync");
return 10;
}
static async Task Main(string[] args)
{
//위에서 task 를 만듬과 동시에 시간은 흐르게 된다
int retValue = await testAsync();
Console.WriteLine("while start " + retValue);
while(true)
{
}
}
}
}
다음처럼 main 에서 다른일을 하기위해 task 를 별도의 변수로 두어 위 async 함수를 분리하여 동시에 처리할 일을 처리 할 수도 있다
using System;
using System.Threading.Tasks;
namespace ConsoleApp2
{
class Program
{
static async Task<int> testAsync()
{
Console.WriteLine("start testAsync");
//3초 후에 task 를 반환한다
await Task.Delay(3000);
Console.WriteLine("end testAsync");
return 10;
}
static async Task Main(string[] args)
{
Task<int> t = testAsync();
Console.WriteLine("다른 일");
//위에서 task 를 만듬과 동시에 시간은 흐르게 된다
int retValue = await t;
Console.WriteLine("while start " + retValue);
while(true)
{
}
}
}
}
예시 다음 처럼 조식을 만들때
다음 처럼 일반적인 방식인 순차적으로 생각 할 수도 있지만
using System;
using System.Threading.Tasks;
namespace AsyncBreakfast
{
// These classes are intentionally empty for the purpose of this example. They are simply marker classes for the purpose of demonstration, contain no properties, and serve no other purpose.
internal class Bacon { }
internal class Coffee { }
internal class Egg { }
internal class Juice { }
internal class Toast { }
class Program
{
static void Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Egg eggs = FryEggs(2);
Console.WriteLine("eggs are ready");
Bacon bacon = FryBacon(3);
Console.WriteLine("bacon is ready");
Toast toast = ToastBread(2);
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("toast is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}
private static Juice PourOJ()
{
Console.WriteLine("Pouring orange juice");
return new Juice();
}
private static void ApplyJam(Toast toast) =>
Console.WriteLine("Putting jam on the toast");
private static void ApplyButter(Toast toast) =>
Console.WriteLine("Putting butter on the toast");
private static Toast ToastBread(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
Task.Delay(3000).Wait();
Console.WriteLine("Remove toast from toaster");
return new Toast();
}
private static Bacon FryBacon(int slices)
{
Console.WriteLine($"putting {slices} slices of bacon in the pan");
Console.WriteLine("cooking first side of bacon...");
Task.Delay(3000).Wait();
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("flipping a slice of bacon");
}
Console.WriteLine("cooking the second side of bacon...");
Task.Delay(3000).Wait();
Console.WriteLine("Put bacon on plate");
return new Bacon();
}
private static Egg FryEggs(int howMany)
{
Console.WriteLine("Warming the egg pan...");
Task.Delay(3000).Wait();
Console.WriteLine($"cracking {howMany} eggs");
Console.WriteLine("cooking the eggs ...");
Task.Delay(3000).Wait();
Console.WriteLine("Put eggs on plate");
return new Egg();
}
private static Coffee PourCoffee()
{
Console.WriteLine("Pouring coffee");
return new Coffee();
}
}
}
async 함수를 적절히 await 하여 처리 하여 대기 시간을 줄일 수 있다
그림은 위에서 아래로 내려가는 방향의 시간의 흐름이고 옆은 비동기로 실행 되는 개수라 보면 된다
Coffee cup = PourCoffee();
Console.WriteLine("Coffee is ready");
Task<Egg> eggsTask = FryEggsAsync(2);
Task<Bacon> baconTask = FryBaconAsync(3);
Task<Toast> toastTask = ToastBreadAsync(2);
Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("Toast is ready");
Juice oj = PourOJ();
Console.WriteLine("Oj is ready");
Egg eggs = await eggsTask;
Console.WriteLine("Eggs are ready");
Bacon bacon = await baconTask;
Console.WriteLine("Bacon is ready");
Console.WriteLine("Breakfast is ready!");
최종 방식은 List 로 Task 를 담아 대기 하는 방식으로 최종 15 분 안에 끝나는 예시이다
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace AsyncBreakfast
{
// These classes are intentionally empty for the purpose of this example. They are simply marker classes for the purpose of demonstration, contain no properties, and serve no other purpose.
internal class Bacon { }
internal class Coffee { }
internal class Egg { }
internal class Juice { }
internal class Toast { }
class Program
{
static async Task Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
var eggsTask = FryEggsAsync(2);
var baconTask = FryBaconAsync(3);
var toastTask = MakeToastWithButterAndJamAsync(2);
var breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask };
while (breakfastTasks.Count > 0)
{
Task finishedTask = await Task.WhenAny(breakfastTasks);
if (finishedTask == eggsTask)
{
Console.WriteLine("eggs are ready");
}
else if (finishedTask == baconTask)
{
Console.WriteLine("bacon is ready");
}
else if (finishedTask == toastTask)
{
Console.WriteLine("toast is ready");
}
await finishedTask;
breakfastTasks.Remove(finishedTask);
}
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}
static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
var toast = await ToastBreadAsync(number);
ApplyButter(toast);
ApplyJam(toast);
return toast;
}
private static Juice PourOJ()
{
Console.WriteLine("Pouring orange juice");
return new Juice();
}
private static void ApplyJam(Toast toast) =>
Console.WriteLine("Putting jam on the toast");
private static void ApplyButter(Toast toast) =>
Console.WriteLine("Putting butter on the toast");
private static async Task<Toast> ToastBreadAsync(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
await Task.Delay(3000);
Console.WriteLine("Remove toast from toaster");
return new Toast();
}
private static async Task<Bacon> FryBaconAsync(int slices)
{
Console.WriteLine($"putting {slices} slices of bacon in the pan");
Console.WriteLine("cooking first side of bacon...");
await Task.Delay(3000);
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("flipping a slice of bacon");
}
Console.WriteLine("cooking the second side of bacon...");
await Task.Delay(3000);
Console.WriteLine("Put bacon on plate");
return new Bacon();
}
private static async Task<Egg> FryEggsAsync(int howMany)
{
Console.WriteLine("Warming the egg pan...");
await Task.Delay(3000);
Console.WriteLine($"cracking {howMany} eggs");
Console.WriteLine("cooking the eggs ...");
await Task.Delay(3000);
Console.WriteLine("Put eggs on plate");
return new Egg();
}
private static Coffee PourCoffee()
{
Console.WriteLine("Pouring coffee");
return new Coffee();
}
}
}
와이파이에서는 100ms 에서 불안정하면 200~300ms 까지도 불안정하게 발생하게 된다
보통 게임에서 30~60 프레임 기준을 많이 사용하는데
60프레임 기준에선 한프레임당 16ms 정도 의 시간이 걸린다
이 시간을 넘어가면 사람들이 인지는 하기 시작하고
30프레임에선 한 프레임당 33 ms 인데 33ms 가 넘어가면 거의 모든 사람들이 부자연 스럽다고 인지하게 된다
[예측과 보정]
가지고 있는 정보들을 통해서 미리 예측을 하고 정보를 받았을때 예측과 결과가 다르다면 보정을 해주는 방식
ex ) 타격이 먼저 되고 때리는 모션이 나중에 나가는 경우
논타겟의 경우 시점 분리가 된다 : 스킬 시전 시점과 스킬 피격 대상 선정 사이에 차이가 발생
만약 스킬 영역에 스킬을 시작하고 스킬이 발생 하는데 까지 시간이 걸리는데 영역안에 있던 대상이 밖으로 도망가면 피격이 되면 안된다, 이때는 피격 당하는 대상의 시점만 미뤄두고 나중에 데미지가 가해지게 하면 되지만 시점분리에 필요한 요구사항이 점점 늘어난다면?
ex) 스킬 대상이 넉백 되면서 데미지는 그 도중에 되면 좋겠다
: 1. 시작은 따로 2. 그다음에 넉백. 3 그다음에 데미지 이렇게 3개로 분리가 된다
영역은 하나인데 대상을 3번에 걸처서 타격해야 하는 상황이 발생한다
더 나아가 쿨 다운은 선딜 후 타격 전에 돌리고 싶다
내가 스킬 시전하다가 넉백을 당하면 취소가 되야하고
기를 모으는 시간에 따라 효과가 다르게 하고 싶다
돌진하면서 주변에 있는 적들을 차례로 밀치고 싶다
등등등.. 요구 사항이 점점 더 많아짐..
이때 생각해 볼 수 있는거 스킬 스테이지 방식이다
스킬을 여러 단계로 나누어서 구성 하는 것을 말한다
그런데 문제는 결정된 시간이 전달이 될때 받는 쪽에서 지연이 되면 내가 원하는 타이밍에 전달되지 않게 된다
아래처럼 동작하겠지 라고 생각하고 만들었는데
네트워크를 타는 순간 스킬 구성이 달라지게 된다
이 처럼 시간 간격에 딜레이가 생기면서 타격이 늦게 일어나거나 여러 가지 시점이 흔들리는 일들이 벌어지게 된다
그래서 나온게 예측 가능하게 만들자
여러 스테이지 결과를 미리 결정 및 공유하고
이에 맞춰 각자 서버와 클라가 스킬 스테이지를 진행한다
그래서 이때 미리 계산 할 수 있는 그룹들을 미리 묶어 두어서 처리하게 된다
그것이 스킬 스테이지 플로우 도입
CapturingTarget 영역 안의 대상 선정
이나 중요하게 반드시 시점이 나뉘어야 하는 것들만 플로우를 나누는 기준이 되고 나머지 스테이지들은 묶어서 미리 계산을 하도록 처리
이걸
이렇게 묶음으로 써
원래 4번 전송 되던걸 두번만 전송하게 되고 묶음 안에서는 어색함 없이진행 가능
문제점 : 플로우가 길다보면 발생할 수 있는문제가 중간에 취소 하면 취소가 늦게 가는 경우
피격을 하지 않았는데 피격 연출이 나온다던가 하는 문제
해결 방안 => 정상 타격이 완료 되면 데미지 숫자를 붉은색으로, 그렇지 않으면 파란색으로 표시하여 대미지가 안들어간것으로 보이게끔 처리
또는 스킬 연출과 별개의 추가 연출 구간을 만들어서
정상타격 완료 시 : 대미지 플로터로 확정 연출
정상타격 취소로 판정시(늦게 패킷이 오면) : 대미지 플로터를 모래 연출처럼 흩어 버리기
방식으로 처리
논 타겟일때 움직이면 맞고 가만히 있으면 맞을때도 있고 안맞을때도 있는데
=> 타격자 시점에서는 상대 캐릭터가 스킬 범위에 들어갔는데 피격자에서는 안맞는 문제가 보임 즉 내 캐릭터가 공격을 하는데 상대 캐릭터가 스킬 범위에 들어왔음에도 불구하고 공격이 가해지지 않음
=> 피격자 시점에서는 타격자가 공격 당하는 상대로 스킬을 논타겟으로 썼지만 상대가 스킬 범위에 들어가 있지 않아서 공격을 받지 않은 상황으로 보이는 중
이건 플레이어 위치가 타격자와 피격자 시점에서 위치가 살짝 다를때 발생 할 수 있다
서버상에서도 피격자의 위치가 실제 달라서 발생
타격자 시점에서 상대방 위치를 보면 조금 더 왼쪽으로 가 있는 상태
이때 이동 시스템이 어떻게 되어 있는지 알 필요가 있다
폴리드 타일기반으로 이동하는 시스템
셀도 없고 방향도 특정되지 않고 자율롭게 돌아다닐 수 있는 구조
위치를 짧은 시간내에 동기화 할 수 있는 모델은 없었다는 것 MMORPG 라
대규모 전투에서 통신량이 큼으로 유저가 많을 수록 제곱에 비례해서 늘어나기 때문에..
[이동에 관한 히스토리]
클라가 서버에게 이동 할래 라는 패킷을 날리면 서버가 판단해서 이동을 시키는 구조
결국 클라이언트에서 먼저 이동하도록 변경
즉시 예상하지 못하는 상황에서 멈춤을 할때 를 생각해 보면 다른 유저, 서버에서의 해당 유저, 내클라이언트에서 다른 유저를 멈출때의 위치가 모두 달라지게 된다
즉 피어들간의 위치 정보의 오차가 커진다
멈추는 클라이언트에서는 빨간 위치에서 멈추라고 했지만 서버와 다른 클라에서 모두 다 조금씩 늦게 받게 되어 느려지는 현상이 발생 할 수 있다
순간적으로 변경한 명령이 다음 이동 이다 라고 하면
이동의 시작 구간에서 보정을 진행하면 된다 이때는 이동을 하는 와중에 스킬을 잘 맞고 위치 오차도 적은 편이다
하지만 바로 멈추라고 하면 다음 이동이 없다, 즉 멈추라고 하면 다음 이동이 없어 보정할 곳이 없다
=> 만약 원래 멈춰야 하는 곳보다 더 갔을때 캐릭터를 뒤로 당기면 이동이 어색해 지기 때문에 그 자리에 있을 수 밖에 없도록 프라시아에서는 작업을 했었고 다음 이동 명령때 다시 보정을 한다를 컨셉이였었다 => 그러다보니 생각하는 위치들이 다 달랐던 것
해결 : 구간이 없어도 강제로 맞춰줘야 되는 부분이기 때문에 기본적인 해결책 부터 적용 시작
방안 :
멈추면 그 자리로 그냥 옮기자
핑이 안정적일때는 40ms 에서 120ms 까지는 그래도 안정적으로 추가 이동명령으로 위치 보정을 해도 어색하지 않고 추가로 멈춰야 할 위치까지 이동해서 보정이 가능하다
그런데 이미 이동을 멈춰야 하는 위치보다 더 가 있을때 이걸 당기는건 게임을 망치기 때문에
그 자리에 멈출 수 밖에 없다 즉 50% 확률이 보정이 되거나 안되거나 하는 상황
정확(무보정)의 수치를 200ms 뒤 까지에서 실행되게 해야하는 것이 목표인데
즉 대부분이 보정이 될 수 있게 처리를 해야 하는데
이렇게 처리 하기 위해선 내쪽에서
다른 개체의 이동 요청이 왔을때 120에 받았는데 마치 200에 받은 것 처럼 처리해 주면 된다
즉 다른 개체의 이동 정보는 한 80ms 늦게 실행 시킨다
이렇게 하면 보정을 더 과하게 해야하긴 하지만 아예 보정을 할 수 없는 케이스는 거의 사라지게 된다
위의 문제는 이동에 관한 갑자기 멈춤에 관한 문제였음으로 상대 캐릭터 이동에 대한걸 80ms 정도 지연시켜 받아서 이동에 대한 보정 처리를 해주면 결과적으로 200ms 내에 이동 보정이 됨으로 타격은 정확한 처리가 된다 => 문제는 해결됐다 수치상 80ms 이긴 한데 상황에 따라 다를 수도 있으니 동적으로 다르게 주는 것도 생각해 볼만한 문제
그런데 이동하는 중에는 오차가 좀 더 커진 셈이긴 하지만 캐릭터가 스킬을 안맞는 크리티컬한걸 피했다고 볼 수 있다
정리
예측을 활용 하는 동기화 방식은 어느정도 활용 중
하지만 모든 정보를 알리는 방식으로 동기화를 할 경우 과다한 네트워크 통신, 느린 반응성등의 문제가 발생 할 수 있다
시간의 흐름에 따른 상태를 계산해 낼 수 있는 일부 상태만 알림을 통해 동기화 처리 한다 => 정해진 규칙에 따라 현재 상태를 도출한다
네트워크를 통하면 외부 요인으로 인해 상태가 변경 되었을 때 이 정보를 내가 늦게 알게 되는 경우가 자주 발생한다,
내가 계산해서 예측을 해서 가지고 있는 현재 상태랑 믿을 수 있는 소스 보틍은 서버와 상태가 서로 다를 경우 현재 내가 가지고 있는 상태를 실제 상태로 자연스럽게 맞추는 작업을 보정이라고 한다
만약 더이상 자주 알려주기 힘들다면 고려해볼만 한 것은
이른 시점에 결정적인 상태를 만든다, 먼저 계산해서 알려준다 는것
도출해낸 결과가 정확한게 가끔 많이 틀릴 수 있다면 엄청 많이 틀리는 것을 좀 완화하고 대신에 평소에는 조금 정확성이 떨어지더라도
워스트 케이스를 없애는 게 더 좋을 수 있다
다만 정확성을 너무 포기하면 보정이 힘들어진다
구간이 없다면 80ms 정도 늦게 받아서 처리 하는 것처럼 구간을 만들어서 구간을 활용하는 전략들을 고민하면 좋을 것
예측에는 Extrapolation 외삽법이 주로 쓰이고 보간에는 interpolation 이 주로 쓰인다
--------------------
별도 추가 부분
--------------------
외삽법 : 외삽법이 뭔지 오래 되서 잘 기억이 안날 수 있음으로 복습차원에서..
검은색이 원래 데이터이고 파란색이 예측하는데이터인데 점3개에 대해서 보간을 하면 원래 검은색과 거의 유사하게 파란색이 나오는데 그 미래에 대한 4번째 점에 대해선 오차가 심해 질 수 있다는 외삽법의 단점이 있다
고차다항식의 보간법의 위험성 : 항이 많아지고 차수가 높아 질 수록 식이 정교해 질것이라는 기대가 있지만 오히려 않좋은 결과가 나올 수도 있다
점선이 정확한 값인데 점 4개를 갖고 4차 다항식을 만들 수 있고 그림을 그리면 검은 선이 나오는데 형태는 조금 유사해도 오차가 나온다
이때 더 많은 점을 도입해서 10차 다항식으로 접합을 해보면
runge 함수를 다항식으로 묘사를 해봤더니 양 끝단에 오차가 상당히 크게 나온걸 알 수 있다
즉 다항식 보간법을 쓸때 고치일 수록 정확하다고 볼 수 없다는 것이 Runge 라는 사람이 만든 Runge 함수이다, 오히려 낮은 차수 일때 더 유사할때가 있다는 것
이글의 원문 :
● 발표분야: 프로그래밍 ● 발표자: 넥슨코리아 정성훈 / Nexon Korea Sunghoon Jung ● 권장 대상: 프로그래머, 시스템 디자이너 ● 키워드: #네트워크 #동기화 #연출 #프라시아전기
이때 서버는 타기에 필요한 검사를 함 캐릭터의 이동 방향을 보고 탈것이 캐릭터를 태우러 갈 수 있는지 확인 필요 이때 네비게이션 메시 상에서도 유효한 경로인지 같이 검사를 한다
부르기 패킷(만날위치포함) : 준비가 끝나면 문제가 없다 판단 되면,
모든 클라에게 탈것에 대한 만날 위치포함하여 부르기 알람 메세지를 보낸다 그리고 탈것이 어디로 갈지에 대한 정보도 포함한다
부르기에 필요한 시간을 기다렸다가 랑데부 알람 메세지를 클라에게 보내기 위해 서버에서 예약한다
랑데부 패킷 : (랑데부는 만남, 만나는 장소라는 뜻)
클라에서 랑데부를 받으면 클라에서 타기지점으로 뛰기 시작한다
탈것을 스폰하고 캐릭터를 향해 달리게 한다 캐릭터는 이 지점을 향해 달려 가는 연출을 한다 이때 캐릭터는 탈것을 부르면서도 계속 달리는 중임으로 부르기 상태에서부터 다른 이동의 간섭을 막고 탈곳과 만나기 위한 곳으로 달려 나간다 이때 캐릭터와 탈것은 계속 달리는 중이다
오르기요청 : 클라가 캐릭터가 탈 준비가 완료 되면 서버에 알려주는 방식 클라에서 서버로 알리는데 => 캐릭터와 탈것은 계속 달리는 중이기 때문에 서버와 메시지를 주고 받는 동안에도 위치가 계속 바뀌고 있는 중임으로 이때 오차가 발생 할 수 있는데 이를 줄이기 위해
캐릭터와 탈것의 위치를 서버로 같이 전달한다
서버에서는 탈것의 위치가 탈것에 올라타는 캐릭터의 새 위치가 되기 때문에 이 둘의 범위가 약속한 범위 안쪽으로 들어오면 모든 클라에게 오르는중패킷을 보낸다
나 자신은 이미 이전에 오르는 연출을 시작 하기 때문에 오르기요청에 대한 응답 패킷은 무시하지만 다른 클라는 오르는중 패킷을 받으면 오르는 연출을 시작한다
오르기 완료 패킷 : 오르는 중이 완료 되는 시간이 정해져 있고 이 시간이 완료 되면 상태와 오르기완료패킷을 클라들에게 보내어 오르기를 완료한다,
위 이미지인데 타는 경로를 아래 처럼 생각 해볼 수 있다
캐릭터 오르는 연출
랑데부 에서
캐릭터와 탈것이 각각 어느 위치에 있을때 오르기가 시작되는지를 결정해야한다 캐릭터가 오르기 시작 애니가 나가고 -> 탈것 기준에서 오르기 시작한 지점과 오르기 완료된 지점까지 완료 되면 이때 약간 더 앞으로 전진하면 원래 캐릭터가 이동하던 직직방향과 만나게 된다, 그리고 오르기 완료시까지 플레이어 입력을 일시적으로 제한한다 일시적으로 제한되는 사이에 연출이 일어나기 때문에 이때의 시간은 기획자가 알아서 제어하도록 테이블로 빼놓음, 말이나 탈것에 따라 연출이 달라질 수 있기 때문 그리고 타는 동안 속력은 기존 그대로 고정한다
오르기 완료
캐릭터 액터를 탈것 액터에 부착하는 방식
캐릭터와 탈것의 위치와 방향은 다르지만 일단 붙여놓고 애니메이션을 재생 하면서 일정시간동안 위치와 방향의 차이가 0이 되게끔 한다
이 시간은 오르기 애니메이션 시간 중에서 캐릭터가 탈것에 닿을 때까지의 시간 , 이때 위치와 방향은 형태와 상관 없이 선형적으로 처리
이때 카메라는 캐릭터 위치를 따라가게 한다음 타기가 완료되면 원래 카메라를 제어하던 곳으로 반환 시켜준다
모바일에서 인터넷이 일시적으로 안되는 상황을 대비해 현재 탈것 상태를 다시 알려주는 처리를 예방 , 클라는 서버에서 주는 상태대로 클라이언트 상황을 맞춰 적용해줘야함 => 현재 위치에서 다음 목표 지점이 다이렉트로 나오는 방식
길이 좁거나 막혀 있다면 탈것이 올 수가 없으니 이때는 제자리에서 바로 탈것을 타도록 한다
캐릭터가담당 서버가 바뀌는 지역으로 이동하는 경우
타려는 와중에 서버 이주가 진행 된다면 서버에서 바로 오르기 완료 상태로 바꾸고 클라이언트에 알려주는 상태로 처리 => 임의의 상태로 바로 전환되게 처리 하는 것이 맞아 떨어짐
클라위치와 탈것이 만나지 못하는 거리로 탈것이 지나간다면 그때는 타지 못하고 지나가고 지나간 이후에 제자리에서 바로 탄 상태가 되도록 처리
내리기 처리 : 빠르게 플레이로 돌아갈 수 있는데 집중
오르기완료 상태라면 내리기 처리를 시작
내리는 중, 내리기 완료 패킷을 서버에서 순차적으로 보내준다
내리기과정은 특별히 진행 될게 없고 서버에서 보내주는대로 처리하기면 하면 됨
내릴때 현재 바라보고 있는 방향으로 달려 나가면서 내리도록 처리하고 위치도 탈것 상태를 전달하면서 같이 모든 클라에게 전달하여 모든 클라이언트에서 같은 곳이 될 수 있도록 처리
내릴때는 탈때와의 반대 과정으로 처리 : 캐릭터와 탈것은 때어내면서 캐릭터 위치는 고정하고 이쪽으로 캐릭터가 이동 되게 하면서 애니메이션 재생
내리는 중 패킷을 클라가 받은 것이라면 내리기 완료 패킷이 오는것은 확정적이라서 먼저 내려서 걸어가고 조작 될 수있게 처리 하면 자연스러워짐 예를 들어 말에서 바로 내리는 순간 공격이 가능하다
곡선이동 자동이동 길잡이
곡선이동 :
캐릭터의 이동은 기본적으로 직선이동이다, 이동 회전이 있긴 한데 이동 경로가 딱딱하다 , 이동회전은 느낌이 딱딱하다
그래서 먼거리를 갈때는 먼저 큰 길을 찾아가고 목적지에 가까운 곳까지 큰길을 통해 이동하는 길 찾기 방식을 사용, 이러면 계속해서 큰길들의 분기점을 지나게 된다
이 분기점을 지날때 급격히 방향이 꺽이는 경우들이 많다
탈것을 타고 빠르게 지나갈때는 문제가 더 잘보이는데 이때 곡선 형태로 이동 할 수 있는 것을 추가, 이 지역은 내비메쉬가 깔린 지역이다
이런 점들에선 곡선형태로 분이점을 지나도록 처리
베지어 곡선 사용 2차 곡선이 항상 중심을 향해 들어오고 중심에서 가도록 하기위해 A1, B1 을 잡는데 각 직선에서 ¼ 지점정도에 만든다 중간 지점을 만들기 위해 B0 지점을 임의로 만든다
이때 샘플값이 균일한 곡선이 나오진 않는데
이것은 시간이 아닌 이동거리를 통해 곡선의 이동 거리를 찾으면 됨
2차 베지어 곡선은 특정 구간의 거리를 계산식 하나로 구할 수 있는 특성이 있다(이것이 다른 곡선과 달리 거리에 대한 이점이 있는 곡선이라 이걸 선택함)
거리를 통해 위치를 찾을때는 정확한 지점을 찾긴 어려우니 근사 값으로 찾음(뉴턴랩스방법) : 뉴턴법은 식조작으로 풀지못하는 식의해의 근사 값을 구하는 법으로 접선으로 구한다
멀리서부터 접선을 그리다 보면 빨간점의 해와 접접 가까워지면서 해를 구하는 것
뉴턴방법 = 뉴턴-랩슨 방법 이라한다 x0 에 해당하는 fx 의 접선을 구하고 이 접선과 만나는 x 축의 점 x1 에 대해 다시 기울기를 구해 이것을 반복하는 방식 으로 근사 해를 구하는 방식중 가장 빠르다
이것을 이용해서 거리기반의 위치를 구한다 spline component는 에르미트 곡선을 사용한다 에르미트 곡선은 거리에 따른 위치 값을 바로 얻어 올 수 있어서 이걸 쓰면 됨
자동길잡이 : 캐릭터의 이동 경로를 미리 보여주는 기능
자동이동이 시작 되는 지점에서 이동해야 하는 경로가 대부분 결정되기 때문에 이 경로를 미리 보여주는 것 이건 나한테만 보여지는 것으로 splinecomponent 를 그대로 이용해서 표현이 가능하다
이글의 원문 :
프라시아 전기에 멋진 탈것 만들기 멋지게 타고, 멋지게 달리고, 멋지게 내리자
● 발표분야: 프로그래밍 ● 발표자: 넥슨코리아 이연석 / Nexon Korea Yeonseok Yi ● 권장 대상: 게임 프로그래머, 클라이언트 프로그래머, UE4 프로그래머
C++20 이전의 C++에서는 필요한 함수 또는 클래스를 불러오기 위해 #include 전처리문을 이용해 왔다. 이런 헤더 파일 방식의 문제는..많지만 그 중에 필자가 가장 크리티컬하게 생각하는 부분은 #include 전처리문을 가리키고 있던 파일의 내용 그대로 치환해버려 헤더에 있는 필요하든 필요하지 않든 상관 없이 정의 되어있는 모든 기능을 포함하게 된다는 것이다. 예를 들어 cmath 헤더 파일에서 정작 내가 필요한 기능은 acos함수 하나 뿐이지만, acos를 사용하기 위해서 나는 헤더에 정의된 모든 함수들을 인클루드하고 컴파일 해야만 한다.
이미 현재 다른 언어들에서는 필요한 기능만을 가져 올 수 있는 기능을 제공하고 있지만 C++은 이번 C++20 스펙의 module을 통해 필요한 것만 가져올 수 있는 기능을 제공한다.
기존 C++시스템과 module 도입 이후의 차이와 이점에 대해서 [여기]에 정리 되어 있으니 살펴 보도록 하자.
모듈 코드 작성하기
앞에서 알아 본바와 같이 모듈의 의도와 개념은 간단 명료하다. 이제 모듈을 사용하기 위해 필요한 것들을 알아 보자. 모듈을 사용하기 위해 우리가 알아야 할 것도 간단 명료하다.
module, import, export 이 세가지 키워드를 기억하자
module : 모듈의 이름을 지정 eg)moduleMath : 모듈의 이름은 'Math'이다.
import : 가져올 모듈의 이름을 지정 eg)importMath : 가져올 대상 모듈의 이름은 'Math'이다.
export : 모듈에서 내보낼 기능(함수)의 인터페이스를 지정 eg)exportint sum(int, int) : 내보낼 함수의 이름은 sum이고 리턴 타입은 int, 인자는 int, int다.
모듈 선언(declaration)
기존 #include 전처리 방식은 컴파일러와 상관 없이 선언은 헤더 파일에 위치하고 구현은 .cpp 파일 적성하는 방식이었다면 module은 각 컴파일러마다 각각 다른 방식으로 작성된다.
cl.exe(Microsoft 비주얼 스튜디오)
cl.exe는 비주얼 스튜디오의 컴파일러 이름이다. cl.exe는 모듈을 선언하기 위해 확장자가 ixx인 모듈 인터페이스 파일을 사용한다. 모듈을 만들기 위해 가장 먼저 할 일은 프로젝트에서 아래와 같이 'C++ 모듈 인터페이스 단위(.ixx)'를 선택하여 파일을 생성한다.
확장자가 .ixx인 모듈 인터페이스 파일을 만들고 내보낼 모듈의 이름과 인터페이스를 작성한다.
// ModuleA.ixx 모듈 인터페이스 파일
export module ModuleA; // 내보낼 모듈의 이름 지정
namespace Foo
{
export int MyIntFunc() // 모듈에서 내보낼 기능(함수)의 인터페이스를 지정
{
return 0;
}
export double MyDoubleFunc()
{
return 0.0;
}
void InternalMyFunc() // 모듈 내부에서만 사용하는 함수
{
}
}
3라인의 'export module ModuleA'는 우리가 함수나 변수를 선언하는 것과 같이 모듈을 만들겠다는 선언이다.
7라인과 12라인의 export가 선언되어 있는 MyIntFunc()와 MyDoubleFunc()는 다른 파일에서 이 모듈을 임포트(import)했을 때 사용할 수 있는 함수라는 것을 의미한다.
17라인의 InternalMyFunc() 함수는 모듈 내부에서만 사용되고 이 모듀을 임포트하는 곳에서는 사용하지 못한다.
위 예제와 같이 선언과 구현을 ixx파일에 모두 작성할 수도 있고 다음 예제와 같이 기존 .h, .cpp 구조 처럼 나눠서 작성할 수도 있다.
// 선언만 있는 ModuleA.ixx 모듈 인터페이스 파일
export module ModuleA; // 내보낼 모듈의 이름 지정
namespace Foo
{
export int MyIntFunc();
export double MyDoubleFunc();
void InternalMyFunc();
}
// ModuleA.cpp 모듈 구현 파일
module ModuleA; // 시작 부분에 모듈 선언을 배치하여 파일 내용이 명명된 모듈(ModuleA)에 속하도록 지정
namespace Foo
{
int MyIntFunc() // 구현에서는 export 키워드가 빠진다.
{
return 0;
}
double MyDoubleFunc()
{
return 0.0;
}
void InternalMyFunc()
{
}
}
Visual Studio 2019 버전 16.2에서는 모듈이 컴파일러에서 완전히 구현 되지 않았기 때문에 모듈을 사용하기 위해서는 /experimental:module 스위치를 활성화 해야 한다. 하지만 이 글을 적는 시점에서 최신버전 컴파일러는 모든 스펙을 다 구현하였으므로 모듈을 사용기 위해서는 최신 버전으로 업데이트할 필요가 있다.
module, import 및 export 선언은 C++20에서 사용할 수 있으며 C++20 컴파일러를 사용한다는 스위치 활성화가 필요하다(/std:c++latest). 보다 자세한 사항은[C++20] 컴파일항목을 참고 하도록 하자.
참고로 필자의 경우 x86 32-bit 프로젝트를 생성해서 모듈을 사용하려고 했을 때 제대로 컴파일 되지 않았다. 비주얼 스튜디오에서 모듈을 사용하기 위해서는 64-bit 모드로 프로젝트를 설정해야 한다.
gcc
gcc의 경우 모듈을 선언하기 위해 .cpp 파일을 사용하지만 컴파일 시 -fmodules-ts 옵션을 이용해 컴파일 해야 한다. gcc에서 모듈을 사용하기 위해서는 gcc 버전 11이상이 필요하다. gcc 11의 설치 방법에 대해서는 [여기]를 참고 하도록 한다. gcc 컴파일에 대한 자세한 방법은 [여기]를 참고하자.
Global Module Fragment
적절하게 번역할 단어를 찾지 못해서 그대로 글로벌 모듈 프래그먼트(Global Module Fragment)라고 부르도록 하겠다. 글로벌 모듈 프래그먼트는 #include와 같은 전처리 지시자를 적어 주는 곳이라 생각하면 된다. 글로벌 모듈 프래그먼트는 익스포트(export)되지 않는다. 여기에 #include를 이용해 헤더를 불러왔다고 하더라도 모듈을 가져다 쓰는 곳에서 include의 내용를 볼 수 없다는 뜻이다.
// math1.ixx
module; // global module fragment (1)
#include <numeric> // #include는 여기 들어간다
#include <vector>
export module math; // module declaration (2)
export int add(int fir, int sec){
return fir + sec;
}
export int getProduct(const std::vector<int>& vec) {
return std::accumulate(vec.begin(), vec.end(), 1, std::multiplies<int>());
}
모듈 불러오기(import)
모듈을 불러와 사용하는 방법은 기존의 include와 매우 비슷하다. #include 대신 import 키워드를 사용한다
필자의 경우 비주얼 스튜디오에서는 #include, import 순서에 상관 없이 정상적으로 컴파일 되었지만, gcc에선 헤더와 모듈의 순서에 따라 컴파일 에러가 발생했었다(내가 뭔가를 잘못해서 그럴 수도 있지만..). 모듈을 임포트 후 shared_ptr을 위해 memory 헤더를 인클루드하면 컴파일 오류가 발생하고, 헤더를 먼저 인클루드 후 모듈을 임포트하면 정상적으로 컴파일 되었다.
언리얼 에서 델리게이트는 C++ 객체에만 사용할 수 있는 델리게이트와 C++, 블루프린트 객체가 모두 사용할 수 있는 델리게이트로 나뉜다. 블루프린트 오브젝트는 멤버 함수에 대한 정보를 저장하고 로딩하는직렬화 매커니즘이 들어있기 때문에일반 C++ 언어가 관리하는 방법으로 멤버 함수를 관리 할수 없다.
그래서 블루프린트와 관련된 C++함수는 모두 UFUNCTION 매크로를 사용해야 한다. 이렇게 블루프린트 객체와도 연동하는 델리게이트를 언리얼 엔진에서는다이내믹 델리게이트라고 한다.
Delegate란?
함수를 바인딩하는 형태로 등록시켜 CallBack함수와 같이 활용 할 수 있습니다.
언리얼 C++에서 충돌감지 컴포넌트 개열에서 AddDynamic 메크로를 통해 출돌시 등록한 CallBack함수를 호출하는 것이 구현 되어 있습니다.
언리얼 C++에는 총 4가지 종류의 Delegate가 있습니다.
델리게이트(싱글 케스트)
가장 기본적인 Delegate로 함수 1개를 바인드하여 사용합니다.
멀티 케스트
싱글 케스트와 동일하지만 여러 함수를 바인드 할 수 있습니다.
이벤트
멀티 케스트와 동일하지만 전역으로 설정할 수 없어 외부 클래스에서 추가 델리게이트 선언이 불가능합니다.
다이나믹 멀티케스트(다이나믹)
다이나믹은 싱글과, 멀티 두개다 존재하며 다이나믹 델리게이트는 직렬화(Serialize)화 되어 블루프린트에서 사용 가능합니다.
바인딩(Bind)란 Delegate에 특정 바인드 함수를 통해 콜백함수를 등록하는 것을 의미합니다.
공식문서에는 여러 형태의 바인드함수가 있지만 이 포스팅에서는 함수 바인딩을 중심적으로 설명하겠습니다.
샘플 프로젝트 소개
샘플 프로젝트는 포스팅 상단에 업로드했습니다.
샘플 프로젝트는 폭탄 오브젝트가 하나 있으며키보드 "B"키는 누르면 점화되어 3초 뒤에 폭발합니다.
프로젝트의 구현 형태는 폭탄 클래스에다 함수들을 바인드시켜놓고 폭탄이 터지면 바인드한 함수들을 호출하여
출력 로그에 해당 함수 내에서 로그 코드가 발동되게 구현했습니다.
기본적인 Delegate 세팅법은 먼저 싱글케스트를 예시로 먼저 설명하겠습니다.
헤더 파일 설명
//! ABoom.h
#include "Boom.generated.h"
//! SingleCast
DECLARE_DELEGATE(FDele_Single);
DECLARE_DELEGATE_OneParam(FDele_Single_OneParam, int32);
UCLASS()
class DELEGATETEST_API ABoom : public AActor
{
GENERATED_BODY()
........
public :
FDele_Single Fuc_DeleSingle;
FDele_Single_OneParam Fuc_DeleSingle_OneParam;
};
헤더 파일에서 Delegate함수를 만들고 DECLARE_DELEGATE()메크로를 통해 델리게이트화 시킵니다.
DECLARE_DELEGATE는 인자 값 없는 함수에 사용하고 DECLARE_DELEGATE_OneParam는 1개의 인자값이 있는 함수에 사용합니다, 만약 인자 값이 2개면 TwoParam을 사용합니다.
언리얼엔진하면은 다룬다고 하면 알아야할 몇가지들이 있다. 그 중에서 언리얼엔진에서 베이스가 되는프로퍼티 시스템인 리플렉션이 있다.
리플렉션이라는 것은 자바나 C#등에선 지원하지만, 언리얼엔진에서 사용되는 C++ 언어에서는 지원하지 않아 언리얼엔진에서 구현되어 있는 시스템이라고 했다.
하지만 난 리플렉션이라는게 뭐길래 엔진에서 구현해서 지원해주는것일까?? 처음들어봐서 자바에서 어떻게 사용되는지 먼저 찾아보았다.
자바에서의 리플렉션 개념
자바에서의 리플렉션이란 객체를 통해 클래스의 정보를분석해 내는 프로그램 기법을 말한다. (투영, 반사 라는 사전적인 의미를 지니고 있다.)
자바의 Reflection은 JVM에서 실행되는 애플리케이션의 런타임 동작을 검사하거나 수정할 수 있는 기능이 필요한 프로그램에서 사용됩니다. 쉽게 말하자면, 클래스의 구조를 개발자가 확인할 수 있고, 값을 가져오거나 메소드를 호출하는데 사용됩니다.
간단히 요약하자면, 컴파일 시간이 아니라 런타임시간에 동적으로 특정 클래스의 정보 객체화를 통해 분석 및 추출해낼수 있는 프로그래밍 기법이라고 표현할수 있다. (개발 과정에서 개발자에게 조금 더 도움이 되는 시스템이라고 다가왔다.)
언리얼 엔진에서의 리플렉션
이런 좋은 시스템이 C++에는 지원하지 않아, 언리얼 엔진에서 구현되어 있는데, 우선 관련 설명을 찾아보면
리플렉션(Reflection)은 프로그램이 실행시간에 자기 자신을 조사하는 기능입니다.
이는 엄청나게 유용한 데다 언리얼 엔진 테크놀로지의 근간을 이루는 것으로, 에디터의 디테일 패널, 시리얼라이제이션, 가비지 콜렉션, 네트워크 리플리케이션, 블루프린트/C++ 커뮤니케이션 등 다수의 시스템에 탑재된 것입니다.
언리얼엔진 홈페이지 중...
위에서 '자기 자신'은 클래스, 구조체, 함수, 멤버 변수, 열거형 등을 의미하고 있다.
수집 된 정보는 UClass에 보관된다. 런타임에는 GetClass() 함수를 통해 접근하고, 컴파일 타임에서는 StaticClass()를 사용해 접근한다. 해당 함수들은 언리얼 헤더 툴에 의해 자동으로 생성된다.
잠깐, 왜 언리얼에서는 프로퍼티 시스템 이라고도 하는가?
전형적으로 이러한 리플렉션은 '프로퍼티 시스템'이라고 부르는데,그 이유로는 아마도 '리플렉션(Reflection/반사)'은그래픽 용어라서 혼돈을 빚을 수 있기 때문인것 같다.(개인적인 추측)
리플렉션을 알려주는 방법?
Unreal Header Tool (UHT)가 그 프로젝트를 컴파일할 때 해당 정보를 수집한다.
헤더(.h)에 리플렉션이 있는 유형으로 알려주려면, 헤더 파일 상단에 특수한 include 를 추가해 줘야 하야한다.
그러면 리플렉션이 있는 유형은 이 파일을 고려해야 한다는것 그리고 시스템 구현에도 필요함을 UHT 에 알려줍니다.
그 #include는 아래와 같다.
#include "FileName.generated.h"
위와 같은 헤더가 추가 된다면, 이제 열겨형/UENUM(), 클래스/UCLASS(), 구조체/USTRUCT(), 함수/UFUNCTION(), 멤버변수/UPROPERTY() 를 사용하여 헤더의 다양한 유형과 멤버 변수 주석을 달 수 있다.
이 매크로 각각은 유형 및 멤버 선언 전에 오며, 추가적인 지정자 키워드를 담을 수 있습니다.
리플렉션작동 원리
UBT(Unreal Build Tool) 는 그 역할을 위해 헤더를 스캔한 다음 리플렉션된 유형이 최소 하나 있는 헤더가 들어있는 모듈을 기억.
그 헤더 중 어떤 것이든 지난 번 컴파일 이후 변경되었다면, UHT 를 실행하여 리플렉션 데이터를 수집하고 업데이트.
UHT 는 헤더를 파싱하고, 리플렉션 데이터 세트를 빌드한 다음, (모듈별.generated.inl 에 기여하는) 리플렉션 데이터가 들어있는 C++ 코드를 생성할 뿐만 아니라, (헤더별 .generated.h 인) 다양한 헬퍼 및 thunk 함수도 생성.
요약 :
1. UBT가 리플렉션 키워드 탐색
2. UHT가 해당 .cpp를 파싱
3. 리플렉션 데이터 정보를 수집
4. 수집된 정보는 별개의 C++ 코드 .generated.h / .cpp로 저장
5. 빌드 시 기존 코드에 .generated.h 코드를 추가해 컴파일
이렇게 UHT에 수집된 리플렉션 데이터는 별개의 코드로 저장되고 클래스이름.generated가 붙는다.
언리얼 실행환경은 이를 사용해 언리얼 오브젝트를 관리하고 에디터에서는 에디터에서 편집할 수 있는 인터페이스를 제공한다.
해당 파일들을 프로젝트 폴더에서 \Intermediate 폴더에 저장 된다. 클래스.h의 내용이 변경될때마다 자동 생성, 기존 파일을 덮어 쓴다.
generated 함수에는 StaticClass() / StaticStruct() 같은 것이 포함되어 있어, 유형에 대한 리플렉션 데이터를 구하는 것이 쉬워질 뿐만 아니라, 블루프린트나 네트워크 리플리케이션에서 C++ 함수를 호출하는 데 사용되는 thunk를 구하는 것도 쉬워집니다. 이는클래스나 구조체의 일부로 선언되어야 하며, GENERATED_UCLASS_BODY() 또는 GENERATED_USTRUCT_BODY() 매크로가 리플렉션된 유형에 포함되어야 하는지에 대한 이유가 됩니다.
이 매크로를 정의하는 #include 'TypeName.generated.h' 는 물론입니다.
According to the following test, it seems that astd::vector<int>increases its capacity in this way:
it happens when wepush_back()andthe capacity is already full (i.e.v.size() == v.capacity()), it has to be noted that it doesn't happen a little bit before
the capacityincreases to 1.5 times the previous capacity
Question: why this 1.5 factor? Is it implementation-dependent? Is it optimal?
Also, is there a way to analyze, in this code, when exactly a reallocation happens? (sometimes maybe the capacity can be increased without moving the first part of the array)
Note: I'm using VC++ 2013
I think an important aspect to the answer of why the factor is 1.5 is preferred over 2.0, is that the standard c++ allocator model doesnotsupport areallocfunction:
If it would exist, and it would be used in std::vector, the mentioned memory fragmentation problem would be minimized in all cases where the factor 1.5 is better compared to e.g. factor 2.0 capacity increase.
The factor of 1.5 (or more precisely the golden ratio) is just optimum when the buffer isalwaysrelocated on every buffer size increase (as the other answers explain).
With realloc and if there is space after the current memory block, it would just be enlarged without copying. If there is no space, realloc call would of course move the datablock. However then the free'd blocks (if this happens multiple times) would not be contiguous anyway. Then it doesnt matter whether 1.5 or 2 is the factor for buffer enlargement... Memory is fragmented anyways.
So areallocapproach in std::vector would help wherever the factor 1.5 has the advantage over 2.0andwould save the data copy.
Damageis a common concept in video games. Because of this, one of the default features in Unreal Engine is a framework for dealing and receiving damage.
This tutorial is the first in a series about damage, and aims to give you an introduction to the Damage system in blueprint and C++.
To borrow a phrase from Alex Forsythe:With Unreal, damage comes standard. It’s a feature that is part of theActorclass, meaning that every actor class that you’ve created so far already natively supports dealing & receiving damage!
Let’s start this tutorial with a quick introduction to the concept of instigator. After that, we’ll look into how damage should be dealt and received, both in blueprint and C++.
If you’ve been working in Unreal, you might already have seen the term “instigator” a couple of times. Every actor has anInstigatorproperty, it is a reference to aPawnand represents the pawn/character responsible for any actions the spawned actor will do, like dealing damage. A very practical example is a cannonball, the instigator would be the pawn firing the cannon.
The instigator is usually set when you spawn the actor. In blueprints, this is done like this:
In C++, you can set the instigator pawn in theFActorSpawnParametersstruct:
void AYourPawn::SpawnProjectile(const TSubclassOf<AActor> ProjectileClass)
{
// FP_MuzzleLocation must be a valid component.
check(FP_MuzzleLocation != nullptr);
// Spawn a projectile at a specified transform, with instigator set to this pawn.
const FTransform& SpawnTransform = FP_MuzzleLocation->GetComponentTransform();
FActorSpawnParameters SpawnParams;
SpawnParams.Instigator = this;
World->SpawnActor<AActor>(ProjectileClass, SpawnTransform, SpawnParams);
}
It is also possible to change the instigator after spawning by simply callingSetInstigator.
The concept of instigators is incredibly useful for damage: It allows a damage receiver to act depending on who was responsible for dealing the damage. The classroom example for this isfriendly fire: If you receive damage and the instigator of the damage is someone on your own team, you might decide to ignore the damage if friendly fire is disabled.
To inflict damage on an actor with blueprints, simply call theApplyDamagefunction:
Let’s briefly talk about some of the parameters:
Damaged Actor: The actor you’re dealing damageto.
Event Instigator: Thecontrollerthat isresponsiblefor dealing this damage. You can obtain the instigator controller using theGetInstigatorControllernode.
Damage Causer: The actor thatactually dealtthis damage (e.g. a projectile or grenade).
Damage Type Class: An object that can influence damage calculations. This deserves a tutorial on it’s own so we will just keep it empty for now.
In addition toApplyDamage, there are also separate, specialized functions for dealing point and radial damage. Point damage is for damage at a particular point, coming from a particular direction (like being hit with a bullet). Radial damage is for damage in a wider area, possibily affecting multiple actors (like a grenade blast). More about point and radial damage in a future tutorial.
The C++ equivalent for dealing damage isAActor::TakeDamage. While the blueprint equivalent has different functions for each damage variant (i.e. point, radius, etc.), theTakeDamagefunction combines all of these into one with aFDamageEventstruct to differentiate between the variants.
Dealing damage in C++ is pretty simple though, here is the equivalent for the above blueprint example:
void AYourActor::DealDamageTo(class AActor* OtherActor)
{
// Much like the blueprint example, we're not using DamageType yet.
OtherActor->TakeDamage(4.2f, FDamageEvent(), GetInstigatorController(), this);
}
For dealing point and radial damage, you will need to supply aFPointDamageEventorFRadialDamageEventwith the proper arguments, instead of theFDamageEvent. For applying radial damage, I recommend using theUGameplayStatics::ApplyRadialDamagehelper function instead.
Responding to damage in an actor is as simple as overriding theAnyDamagefunction in blueprint. For C++ things are a bit more tricky, we’ll get to that after the blueprint stuff.
In addition to theanydamage function, there are also separate functions for receivingpointandradialdamage. Keep in mind that when you receive point or radial damage,AnyDamagewill still get called!
The following is an example of responding to damage blueprint, simply override theAnyDamagefunction:
Most of the function arguments here are the same as the values you provided to theApplyDamagefunction. The only exception isDamage Type, which is now an object reference rather than a class reference, more info on this in a future tutorial.
In addition toAnyDamage, there is also theOnTakeAnyDamageevent dispatcher. This event is particularly useful if you have a component that takes care of dealing with damage, rather than the actor itself:
Unfortunately, the C++ functionReceiveAnyDamageinAActorwas not made virtual by Epic, so it cannot be overridden. The next best way is to override theTakeDamagefunction instead (InternalTakeRadialDamageandInternalTakePointDamageexist for radial and point damage respectively):
float AYourActor::TakeDamage(const float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
// If you need the DamageType object like in blueprint, this is how you do it:
UDamageType const* const DamageTypeCDO = DamageEvent.DamageTypeClass ? DamageEvent.DamageTypeClass->GetDefaultObject<UDamageType>() : GetDefault<UDamageType>();
// Make sure to call Super so blueprint & event dispatchers still fire.
return Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
}
Fortunately, theOnTakeAnyDamagedelegate can simply be bound to in C++, just like with blueprint:
This tutorial has covered the basics of damage: It introduced the concept instigators, how damage should be dealt and how it should be received, both in blueprint and C++.
Clustered 는 Leaf Page 에 데이터가 들어가고 이전까지 보던 방식 Non-Clustered 같은 경우는 RID 를 통해 한번 경유하여 데이터를 저장하는 방식이다
Clustered 영한사전 , Non-Clustered 색인
Clustered : 실제로 논리적으로 정렬 되는 데이터 순서가 키 값 정렬 순서가 된다 Leaf Page = Data Page 끝 노드에 실제 데이터가 들어가는 형태 데이터는 clustered index 키 순서로 정렬
Non-Clustered 는 clustered index 유무에 따라서 다르게 동작한다
Non-Clustered 인데 clustered index 가 없는 경우(추가 안한 경우) leaf page 에는 heap table 에 대한 heap rid가 존재하여 이 rid 로 -> Heap table 에 접근 데이터 얻어온다 > heap rid = Heap table 에서 어느 주소에 몇번째 슬롯에 데이터가 있는지를 알 수 있는 RID > 데이터는 heap table 이 있고 이곳에 데이터들이 저장된다 (즉 leaf Page 가 인덱스 페이지가 아니고 데이터 페이지가 아니라는 얘기)
Non-Clustered 가 키가 있는데 이때 clustered index 를 추가 하게 되면 heap table 이 없고 leaf page 에 데이터가 있는 상태이다 => leaf table 에 실제 데이터가 있게된다 >heap table 이 없는 대신에 Clustered Index 의 실제 데이터 키 값을 들고 있게 된다, 이 키 값을 통해 cluster page 에서 데이터를 찾게 된다 > Non clustered 에서 먼저 트리를 통해서 자식 중에서 cluster id에 를 찾고 이 id를 통해 cluster index 가 이루고 있는 트리 부모로 가서 여기서 다시 id 를 찾는 형태가 된다 Non-Clustered 가 기존에 있을때 clustered index 를 추가 하면 기존에 Non-Clustered 도 영향을 주어 한단계 더 찾게 된다
기존 테이블을 지우고 테이블을 새로 하나 만든 다음 non clustered index 를 생성 하여 테스트
select *
into TestORderDetails
from [Order Details];
select *
from TestORderDetails;
--non clustered index 를 생성
create index Index_OrderDetails
ON TestOrderDetails(orderID, ProductID);
--TestORderDetails 테이블에 있는 인덱스 정보 보기
exec sp_helpindex 'TestORderDetails';
TestORderDetails 테이블에 있는 인덱스 정보는 다음과 같다는걸 알 수 있다
인덱스 번호를 찾아 보려면 다음 처럼 작성하면 된다
sys.indexes 에서 오브젝트 id 를 비교하여 TestORderDetails 오브젝트 id 롸 같은 오브젝트의 index_id 들을 가져올 수 있다
select index_id, name
from sys.indexes
where object_id = object_id('TestORderDetails');
TestORderDetails 의 index_id 는 2 번이고 2 번으로 인덱스를 담고 있는 인덱스 페이지들을 볼 수 있다
--Index_OrderDetails Index_id 가 2 임으로 2를 인저로 넣어 페이지들을 보기
DBCC IND('Northwind', 'TestORderDetails', 2);
1048 이 root 고 나머지가 자식으로 들어간 걸 indexLevel 을 통해 알 수 있다
1048
1008 1016 1017 1018 1019 1020
이 중에서 1008 페이지 정보를 보면
--페이지 정보 보기
DBCC PAGE('Northwind', 1 , 1008, 3);
이 처럼 HEAP RID 가 있는 것을 알 수 있다 (index 를 non clustered index 로 생성했기 때문)
Heap RID = ([페이지 주소(4)[파일ID(2)[슬롯(2)] ROW) 의 바이트들로 구성 되는데이 ID 를 통해 Heap Table 테이블에서
Heap Table = [{page} {page}{page}{page}]
몇번째 페이지 몇번 슬롯으로 되어 있는지를 Heap RID가 가리키고 있기 때문에
1008 에서 Heap RID 를 통해서 Heap Table 에 있는 데이터를 바로 검색해 찾을 수 있게 된다
DBCC 로 페이지 정보를 볼때 나오는 PageType 은
1 : Data Page
2 : Index Page
를 나타낸다
현재 까지 상황은
NonClusted Index 가 추가 된 상황이고 여기서 Clustered index 를 추가 해보면
-- clustered index 추가
create clustered index Index_OrderDetails_Clustered
ON TestOrderDetails(orderID);
이렇게 추가 된것을 볼 수 있다
이때 다시 2번 Non Clustered index 를 살펴보면 어떻게 달라져는지를 보면
cluster index 추가 하기 전
cluster index 추가 한후
페이지 번호가 바뀐 것을 알 수 있다
기존에는
1048
1008 1016 1017 1018 1019 1020
이런 번호였지만
1024 부터 자식이 시작 하는 걸 알 수 있는데 clustered index 를 추가 하게 되면
1072
1024 10321033103410351036
로 바뀌었다, 이때 1024 페이지 정보를 보면
--페이지 정보 보기
DBCC PAGE('Northwind', 1 , 1024, 3);
원래는 Heap Rid 가 기존엔 있었지만 clustered index 를 추가하여 Heap Rid 가 없어진 것을 알 수 있다
그리고 페이지 id 가 전과 후가 달라진 것도 이전엔 Heap Rid로 heap table 을 찾는 형식이였지만
이젠 직접적으로 찾게 되니 PagePid 또한 달라지게 된 것
clustered index 를 생성할때 OrderID 에 대해 만들었는데
중복된 OrderID 를 볼 수 있는데 이것은 같은데이터에 한하여 동일한 ID 가 붙는 것이고 동일한 데이터를 구분하기 위하여
UNIQUIFIER 필드가 추가 되어 이들을 구분하게 끔 되어 있다
OrderID + UNIQUIFIER 조합으로 식별한다
정리하자면 Non clustered index 에서 Clustered index 를 추가 하면 기존 Heap RID 가 날라가게 된다는 것이다
위에서 본건 Non clusteered 인덱스 가 있었던건 본것이였는데
이번엔 그에 반해 위에서 새로 추가 했던 clustered 를 보면
--TestORderDetails 테이블에 있는 Index_id 보기
select index_id, name
from sys.indexes
where object_id = object_id('TestORderDetails');
테이블에 있는 인덱스 중에서 인덱스 1번은 검색
--Index_OrderDetails Index_id 가 2 임으로 2를 인저로 넣어 페이지들을 보기
DBCC IND('Northwind', 'TestORderDetails', 1);
9184
9144 915291539154915591569157915891599160
이렇게 구성 되어 있다 즉 여기서 알수 있는건
DBCC 로 페이지 정보를 볼때 나오는PageType은
1 : Data Page
2 : Index Page
였기 때문에 PageType 을 보면 PagePID 가 갖고 있는것이 바로 데이터 페이지가 된다는 걸 알수 있다
정리하자면
Clustered index 있으면 leaf page 가 바로 데이터가 되는것이고, Heap Table 또한 없다
Clustered index 가 없다면 Heap RID로 Heap Table 을 검색해서 데이터를 찾는 한번 경유하는 형태가 된다는 것이다 이때는 Heap Table 또한 생성되게 된다
use Northwind;
SELECT *
FROM [Order Details]
ORDER BY OrderID;
select *
into TestORderDetails
from [Order Details];
select *
from TestORderDetails;
--복합 인덱스 추가
create index Index_TestOrderDetails
ON TestOrderDetails(orderID, ProductID);
--인덱스 정보 보기
exec sp_helpindex 'TestORderDetails';
--Index Seek 이건 인덱스를 활용해 빨리 찾는 것인데
--Index Scan 은 Index Full Scan 으로 전체 다 찾는 거라 느리다
--아래 처럼 and 를 쓰면서 인덱스통하여 검색을 하면 Index Seek 가 일어난 다는 걸 알 수 있다
select *
from TestORderDetails
where OrderID = 10248 and ProductID = 11;
--index seek 으로 검색
select *
from TestORderDetails
where OrderID = 10248;
--table scan 으로 찾음
--이유는 ON TestOrderDetails(orderID, ProductID) 이렇게 생성했을대 orderID 가 첫번째에 있었기 때문에
--orderID 로 찾기를 시작할땐 index seek 로 1차적으로 찾는데
--그렇지 않으면 ProductID 로 찾게 되면 2차적으로 정렬하여 table scan 으로 찾게 된다
select *
from TestORderDetails
where ProductID = 10248;
--index 정보 보기
DBCC IND('Northwind', 'TestORderDetails', 2);
DBCC PAGE('Northwind', 1 , 9160, 3);
복합 인덱스를 두개에대해 추가(orderID, ProductID) 했다 create index Index_TestOrderDetails ON TestOrderDetails(orderID, ProductID);
이렇게 하면 데이터를 찾을때 성능에 대한 차이가 발생 하는 이유는 데이터를 정렬 하는데 있어서 차이가 있어서 그런것인데 인덱스가 두개라서 어떻게 검색에 대한 처리를 하는지 보면
--index 정보 보기
DBCC IND('Northwind', 'TestORderDetails', 2);
9200이 가장 상단이고 나머지는 모두 자식 즉 Leaf 노드 인 것을 알 수 있다(IndexLevel 이 0 임으로)
Leaf 노드의 순서가 트리상 동일한 레벨에서 9168 -> 9169 -> 9170-> 이런 순으로 나가기 때문에
9160에 대한 노드 정보를 보면
OrderID 와 ProductID 두개를 보면 우선 OrderID 로 정렬을 하는데 만약 같은 ID 라면 그다음 ProductID로 비교하는 걸 알 수있다
OrderID 는 정렬이 되어 있는 반면 ProductID 는 첫번째 것을 기준으로 정렬 되어 있어서
바로 ProductID로 찾으려 하면 정렬이 안되어 있는것과 마찬가지 임으로
ProductID 로 검색 기준을 삼으면 Scan 이 되는 것 => 느림
인덱스(A, B) 를 글었다면 인덱스 A 에 대해 따로 인덱스를 걸어줄 필요는 없는데
B로도 검색이 필요하다면 인덱스(B) 에 대해 별도로 걸어줘야 빨라지게 된다
주의 : A,B 둘다 인덱스가 독립적으로 걸려있는게 아니다
페이지 분할(SPLIT) : 정리하면 인덱스를 생성하면 원소들이 페이지 단위로 관리 되는데 이 페이지 단위마다 관리 개수를 넘어가면 추가 페이지를 만들어 원소들을 관리하는 구조가 된다
데이터 50개 강제로 추가 해보면 인덱스 데이터 추가 /갱신/ 삭제시 그 중에서 추가 하는 경우를 보자
DECLARE @i INT = 0;
WHILE @i < 50
BEGIN
INSERT INTO TestORderDetails
VALUES (10248, 100 + @i, 10, 1, 0);
SET @i = @i +1;
END
이렇게 하면 추가 페이지가 만들어지는데 그 이유는 한 페이지에 내용이 너무 많아 지면 기존 페이지에 있던 요소 를 두개,세개 로 분할하여 담기 때문이 새로 페이지가 추가된것을 알 수 있고 이것을 페이지 Split 이라 한다
주의 : 인덱스를 Name 에 대해 걸어 놓았을때 SUBSTRING(NAME,1 ,2) 을 하게 되면 인덱스를 사용하는 최적화 사용이 불가능함으로 Index Scan 을 하게 되는 경우가 발생할 수 있다
SUBSTRING 으로 문자를 일부 가져와 어떤 문자를 찾는 것이 아닌
WHERE Name LIKE 'Ba%' ; 식으로 찾으면 Name 에 대해 최적화를 적용 받을 수 있다
실행 할때 Ctrl + L 로 Index Seek 인디 Index Scan 인지 확인하는 것이 좋다
USE Northwind;
--db 정보 보기
EXEC sp_helpdb 'Northwind';
create table Test
(
EmployeeID INT NOT NULL,
LastName NVARCHAR(20) NULL,
FirstName NVARCHAR(20) NULL,
HireDate DATETIME NULL,
);
SELECT *
FROM Test;
--Employees 데이터를 Test 에 추가한다
INSERT INTO Test
SELECT EmployeeID, LastName, FirstName, HireDate
FROM Employees;
RESULT
--index 를 걸 컬럼= LastName
--FILLFACTOR 리프 페이지 공간 1%만 사용 , 전체 사용 공간 중에 1%만 사용 하겠다는 것, 이러면 데이터가 다 안들어가기 때문에 별도 트리구조로 데이터를 저장함
--PAD_INDEX (FILFACTOR 가 중간 페이지에도 적용되게 하는 것) => 결과적으로 공간을 비효율적으로 사용하게끔 하는 것인데 목적이 테스트 하는 것임
CREATE INDEX Test_Index ON Test(LastName)
WITH (FILLFACTOR = 1, PAD_INDEX = ON)
--인덱스가 만들어진 sys.indexes 에서 index_id와 name 을 출력하는데 조건이
--Test 에 만든 인덱스 id 와 같은것만 출력한다
SELECT index_id, name
FROM sys.indexes
WHERE object_id = object_id('Test');
결과 화면은 이런데 여기서 인덱스 Test_Index 를 조사해보면
--2번 인덱스 정보 살펴보기
DBCC IND('Northwind','Test', 2);
빨간색 박스의 IndexLevel 이
가장 높은 숫자가 트리의 Root 이고
2 -> 1 -> 0
순으로 자식을 이루고 있는 인덱스 트리를 생각 하면 된다
2 가 가장 상단 그다음에 1에 해당 하는 것이 PagePID 보면 1008, 984 두개가 있는데
이 중에서 NextPagePID 를 1 레벨에서 다음을 말하는 것이 984 라고 나와 있는 것
즉 순서가 985 -> 1008 -> 984 로 되어 있다는 것을 알 수 있다
트리 형태로 보면 가장 상단 레벨 1에 1개 2 레벨에 2개 3 레벨에 3개의 노드가 있다고 생각하면 된다
그런데 각 노드는 페이지들인데 이 정보들 또한 볼 수 있다
끝단 3개의 노드를 출력해보면
--1 파일번호, 968 페이지 번호, 3 출력 옵션
DBCC PAGE('Northwind',1, 968, 3);
DBCC PAGE('Northwind',1, 976, 3);
DBCC PAGE('Northwind',1, 977, 3);
위 처럼 되고
원래 추가된 9개 와 비교해보면 3개씩 나뉘어져 들어가 있는 것을 알 수 있다
HEAP RID 라는 걸 볼 수 있는데
HEAP RID = 페이지주소(4바이트) , 파일 ID(2), 슬롯번호(2), 로 조합된 8바이트 ROW 식별자이고 이걸로
테이블에서 정보를 추출한다
TABLE [{PAGE} {PAGE}{PAGE} ....] 여기서 찾는 형태이다
RANDOM ACCESS (한건을 읽기 위해 한 페이지씩 접근 하는것 으로 트리 위에서 부터 아래로 노드를 찾아가는 과정)
BOOKMARK LOOKUP : 노드를 찾은다음 RID를 통해 행을 실제 TABLE 에서 찾는 것을 말함
파티션의 경우 playerID로 Group by 와 유사하게 할수 있는데 다른 점은 Group by 는 하나로 묶어 버리지만 Partition 의 경우에는 각 행별로 모두 보이게 된다
결과
PlayerID 별로 랭킹이 매겨져 있는 걸 볼 수가 있다
LAG(바로 이전 : 나열된것의 바로 위) ,LEAD(바로 다음 : 나열된 것을 바로 아래)
결과
playerID 기준으로 salary 값 위아래를 보면 prevSalary 와 nextSalary 값이 있다는 것을 알 수 있다
FIRST_VALUE : 가장 처음 값 : 여금 여기선 큰
LAST_VALUE : 가장 마지막 값 : 지금 여기선 작은 값
결과를 보면 WORST 가 제대로된 작은 값이 아닌데
그 이유는 Desc 로 정렬하여 큰 값은 나오게 되는데 나오는 검색 과정이
동일 아이디의 가장 위에서 부터 하나씩 증가 시키면서 그 중에 큰걸 보게 되는 순서라
4500000
4500000 , 2750000
4500000 , 2750000, 500000
이런순을 검색하면서 그 중 가장 큰걸 뽑아오는게 FIRST_VALUE 라서 best 는 제대로 되지만 worst 는 제대로 되지 않는다는걸 알 수 있다
그래서 LAST_VALUE 의 경우에는 제일 처음부터 증가하는 형태인 키워드 (UNBOUNDED PRECEDING) 를 넣어주고 현재 까지의 진행라인에 대한 현재 CURRENT_ROW 까지 는 BEST 라인이 범위가 되도록 ROWS 를 지정해주면 되고
그다음 LAST_VALUE 에대해선 현재 라인 CURRENT_ROW 부터 동일 아이디에 대해서 마지막 라인을 얘기하는 UNBOUNDED FOLLOWING 을 넣어주면제대로된 WORST 값이 나오게 된다
제대로된 결과
오라클에서 OVER 절에 WINDOWING 절을 처음 사용할 때 ROWS와 RANGE가 어떤 차이점이 있는지 많이 헷갈릴 수 있다. 간단히 설명하면 ROWS는 각 행의 위치고, RANGE는 값의 범위라고 생각하면 된다. 아래의 예제를 여러 번 반복해서 보면 많이 어렵지 않게 이해할 수 있을 것이다.
게임, 전화, 디스플레이, 네트워크 데이터 등등 각각의 프로세스별로 레디큐를 두어 처리 하는 방식이다
(게임 ,전화 등등이 우선순위가 하나의 기준으로 우선순위가 처리되지 않음으로)
각 우선순위를 위한 분리된 큐
위에 있는 것일 수록 우선순위가 높음
각각 레디큐가 별도고 가장 높은 우선순위의 큐가 비게 되면 그다음 system process 의 큐를 실행하고
system process 도 다 끝나면 그다음 interactive process (예를 들어 ui같은) 를 그다음 순위로 처리하는 방식이다
그런데 이때 문제점이 있는데
가장 상단의 우선순위가 높기 때문에 아래 있는 것일 수록 실행 되지 않는 기아상태가 발생할 수도 있기 때문에
그래서 우선순위가 가장 높은 큐는 퀀텀을 8 을 줘서 짧게 실행하게 하고 나가고 그다음 우순위에선 퀀텀 시간을 16을 줘서 좀 더 실행하게 하고 그다음 후순위는 FCFS 로 아래로 갈 수록 더 많은 CPU Burst time 을 할당 받기 때문에 아래 있는 것도 우선순위가 낮아 대기는 하지만 한번 실행될때 대기 한 만큼 좀 더 실행이 되도록 하여 어느정도 균형을 좀 맞출수 있는 형태가 된다
그런데 현대적 컴퓨터에선 프로세스 스케줄링을 하지 않고 스레드 스케줄링을 한다
그런데 지금까지 본것은 프로세스 스케줄링인데 스레드 스케줄링은 프로세스 스케줄링에 견주어 생각해보면 된다
그리고 프로세스에는 커널스레드와 유저스레드가 있는데 이 중에서도 커널 스레드만 스케줄링 하면 되는데
유저 스레드는 스레드 라이브러리에 의해서 관리 되는 소프트웨어적인 측면이기 때문에 커널 유저스레드에 대해 모른다