ãåç¥ã®ãšãããRTSã®äžçã¯ä»ãè¡°éããŠããŸãã ç¬ç«ç³»éçºè ã¯ãã¬ãããªãã©ãããã©ãŒããŒãéåžžã«è€éãªã«ãŒãžã¥ã®ãããªã²ãŒã ããªãããããã®ã«å¿ããããŠããããããã°ããåã«ãå¢çãã§ãã¬ã€ããåŸãç§èªèº«ã䌌ããããªãã®ãå®è£ ããããšæ±ºããŸãã-ã¢ã€ãã¢ã¯é¢çœãã£ãã§ãæè¡çããã³ã²ãŒã ãã¬ã€ã®èŠ³ç¹ã ã²ãŒã éçºã®å®åçµéšãããïŒä»¥åXNAã§äœããããããšããŠããïŒã®ã§ãå°ãªããšãããçšåºŠã®æåãåããã«ã¯ãããé«ã¬ãã«ã§ã·ã³ãã«ãªãã®ã䜿çšããå¿ èŠããããšæããŸããã ç§ãéžãã ã®ã¯Unity 3Dã§ãããUnity3Dã®5çªç®ã®ããŒãžã§ã³ã¯å ±éãããŠããŸããã
ç±æã®è»ãå®æããã°ããã®ãå¢çãããã€ã³ã¹ãã¬ãŒã·ã§ã³ãåŸãŠãUnityã®äžé£ã®ãããªãã¥ãŒããªã¢ã«ãèŠãŠãUnity EditorãæäŸããããŒã«ãã¹ã±ããããŠç¥ãåããŸããã
ã³ãã¥ããã£ãæäŸãããã®
ãã€ãã®ããã«ãç§ã®æåã®ãã³ã±ãŒãã¯ãŽããŽãåºãŠããŸããã ååã«èããã«ããã®å¹³é¢ã®é ç¹ãäžäžãããã¯ãã®å¹³é¢ãšã³ãŒãã䜿çšããŠãã©ã³ãã¹ã±ãŒããå®è£ ãå§ããŸããã å°ãªããšãUnityã«å°ã粟éããŠããå€ãã®èªè ã¯ããUnityã«ã¯ãã®ç®çã®ããã«ç¹å¥ã«èšèšãããå°åœ¢ã³ã³ããŒãã³ãããããŸãïŒããšå察ãããããããŸããã å¯äžã®åé¡ã¯ãç§ã®ã¢ã€ãã¢ã®å®è£ ã«ããŸãã«ãç±å¿ã§ããããã1ã€ã®éèŠãªããšãå¿ããŠããããšã§ãïŒRTFMïŒ ããã¥ã¡ã³ããŒã·ã§ã³ãšãã©ãŒã©ã ãããå°ãæ éã«ç 究ããŠãããªãããã®ãããªå ¬ç¶ãšæããªæ¹æ³ã§åé¡ã解決ããããšã¯ã§ããªãã£ãã§ãããããããã«æ¢è£œã®ã³ã³ããŒãã³ãã䜿çšããã§ãããã
ç¡é§ãªæ±ãšã¢ã«ãŽãªãºã ã®2æ¥éã®åŸïŒå¹³é¢ã¯æããã«ãã®ãããªç®çã§ã®äœ¿çšãæå³ããŠããŸããã§ããïŒãç§ã¯å°åœ¢ã䜿çšããŠå°åœ¢ãäœãå§ããŸããã Unityã³ãã¥ããã£ã®åã ã®ã¡ã³ããŒã®éã§ãã¢ã€ãã¢ã¯åœŒãã®ã²ãŒã ã«ãã€ãããã¯ãªã©ã³ãã¹ã±ãŒããäœæããããšã ã£ããšèšããªããã°ãªããŸããã äžéšã®äººã ã¯ãã©ãŒã©ã ã§è³ªåãããåçãåŸãŸããã SetHeightsã¡ãœããã䜿çšããããšããå§ãããŸãããã®ã¡ãœããã¯ãéžæãããã©ã³ãã¹ã±ãŒãã®ãã€ã³ãïŒxBase; yBaseïŒããéå§ããŠèšå®ããã0fãã1fã®ãã€ããããã®äžéšãåãåããŸãã
æ€çŽ¢ã®çµæã«æºè¶³ããéçºãå§ããŸããã æåã®å®çšãããã¿ã€ãã¯ãæ°æéåŸã«æºåãæŽããæãåçŽãªã«ã¡ã©ãšã³ãžã³ïŒå³ããŒã䜿çšïŒãåçŽãªã¯ã¬ãŒã¿ãŒãžã§ãã¬ãŒã¿ãŒãããã³å·ŠããŒãæŒãããšã§å®éã«ãããã®ã¯ã¬ãŒã¿ãŒãå°åœ¢ã«è¿œå ããŸããïŒãã«ããã€ããã§ããã§ç¢ºèªã§ããŸã ïŒã
å€åœ¢éšåèªäœã¯éåžžã«åçŽã§ããã
ãã®ãããªã¹ã¯ãªããã§ãã
void ApplyAt(DeformationData data) { currentHeights = data.terrainData.GetHeights(data.X - data.W/2, data.Y - data.H/2, W, H); for (int i = 0; i < data.W; i++) { for (int j = 0; j < data.H; j++) { if (data.Type == DeformationType.Additive) currentHeights[i, j] += data.heightmap[i, j]; else currentHeights[i, j] *= data.heightmap[i, j]; } } data.terrainData.SetHeights(data.X - data.W/2, data.Y - data.H/2, currentHeights); }
DeformationDataãªããžã§ã¯ãã«ã¯ãå€åœ¢ãé©çšããX座æšãšY座æšãçŸåšã®å°åœ¢ã«ä»å çãŸãã¯ä¹æ³çã«éç³ãããæ£èŠåããããã€ãããããããã³å€åœ¢ã¡ã«ããºã ãæ©èœããããã«å¿ èŠãªä»ã®å®åæãå«ãŸããŠããŸããã
æªã¿çºçåšããããããšãã°ã
æå®ããããã©ã¡ãŒã¿ã«åŸã£ãŠã¯ã¬ãŒã¿ãŒãçæããŸã
// - H, W . // , H - , W - . float GetDepth(float distanceToCenter, int holeDepth, int wallsHeight, int holeRadius, int craterRadius) { if (distanceToCenter <= holeRadius) { return (Mathf.Pow(distanceToCenter, 2) * (holeDepth + wallsHeight) / Mathf.Pow(holeRadius, 2)) - holeDepth; } else if (distanceToCenter <= craterRadius) { return Mathf.Pow(craterRadius - distanceToCenter, 2) * wallsHeight / Mathf.Pow(craterRadius - holeRadius, 2); } else return 0f; } float[,] Generate(int holeDepth, int wallsHeight, int holeRadius, int craterRadius) { var heightmap = new float[W, H]; for (var x = 0; x < W; x++) { for (var y = 0; y < H; y++) { var offsetX = x - W / 2; var offsetY = y - H / 2; var depth = GetDepth(Mathf.Sqrt(offsetX * offsetX + offsetY * offsetY), holeDepth, wallsHeight, holeRadius, craterRadius); heightmap[x, y] = depth; } } }
ãããŠãããããã¹ãŠããããã°ããã¹ãŠã®Tech Demoã®åºç€ã§ããã
æåã®è©Šè¡ã®çµæã®åæ
Tech DemoãèŠããšãå€åœ¢ã¡ã«ããºã ã«ç¹å®ã®åé¡ãããããšã«ããæ°ä»ãã§ãããã ããªãããããèŠãªãã£ããªãïŒç§ã¯ããªãã責ããªãïŒãç§ã¯ããªãã«äœãééã£ãŠãããæããŸãã äž»ãªåé¡ã¯ããã©ãŒãã³ã¹ã§ããã ããæ£ç¢ºã«ã¯ããã®å®å šãªäžåšã å€åœ¢ãå§ãŸããšããã¬ãŒã ã¬ãŒãã¯éåžžã«å°ããªå€ã«ãªããŸããïŒäžéšã®ãã·ã³ã§ã¯äžæïŒãããã¯ãã²ãŒã å ã«ã°ã©ãã£ãã¯ã¹ãæ¬è³ªçã«ãªãã£ããããåãå ¥ããããŸããã§ããã SetHeightsïŒïŒã¡ãœããèªäœãã©ã³ãã¹ã±ãŒãã«å¯ŸããŠéåžžã«è€éãªäžé£ã®LODèšç®ãåŒãèµ·ããããããªã¢ã«ã¿ã€ã ã®å°åœ¢å€åœ¢ã«ã¯é©ããŠããªãããšãããããŸããã ç§ã®åžæã¯åŽ©å£ããUnityã§ã®ãªã¢ã«ã¿ã€ã ããã©ã¡ãŒã·ã§ã³ã®å®è£ ã¯äžå¯èœã«æããŸãããç§ã¯ãããããã«ãLODåèšç®ã¡ã«ããºã ã®æããã ãéåžžã«éèŠãªæ©èœãèŠã€ããŸããã
å°åœ¢æšé«ãããã®è§£å床ãäœãã»ã©ãSetHeightsïŒïŒã䜿çšãããšãã®ããã©ãŒãã³ã¹ãžã®åœ±é¿ã¯å°ãããªããŸãã
é«ããããã®è§£å床ã¯ãã©ã³ãã¹ã±ãŒã衚瀺ã®å質ãç¹åŸŽä»ãããã©ã¡ãŒã¿ãŒã§ãã ïŒæããã«ïŒæŽæ°ã§ãããããäžã®ã¹ããããã§æŽæ°ã䜿çšããŠå°å³äžã®åº§æšã瀺ããŠããŸãã ãŸããããšãã°256x256ã®ã©ã³ãã¹ã±ãŒãã®å Žåãã©ã³ãã¹ã±ãŒãã®ãµã€ãºãã倧ããããããšãã§ããŸããããã«ãããé«ããããã®è§£å床ã513ã«èšå®ã§ããŸããããã«ãããã©ã³ãã¹ã±ãŒãã®ç²ŸåºŠãšè§åºŠã®ãªã茪éãåŸãããŸãã ãªã512ã§ã¯ãªã513ãªã®ãã次ã®ã»ã¯ã·ã§ã³ã§èª¬æããŸãã
é«ããããã®è§£å床ãæã€ã²ãŒã ã§ã¯ãæ§æã«æé©ãªãµã€ãºãå€å°èŠã€ããããšãã§ããŸããããçµæã«ã¯éåžžã«å€±æããŸããã ãã®ãããªã©ã³ãã¹ã±ãŒããRTSã§æ£åžžã«é©çšããã«ã¯ãå°ãªããšã2人ã®ãã¬ãŒã€ãŒããã°ããã®éå ±åã§ããããã«ããã®ãµã€ãºãååã«å€§ãããªããã°ãªããŸããã ç§ã®æåã®èŠç©ããã«ãããšã2x2kmïŒãŸãã¯2048x2048 Unity UnitsïŒã®ã«ãŒããã¡ããã©ããã£ãã¯ãã§ãã ãªã¢ã«ã¿ã€ã ã§å€åœ¢ã®ãã¬ãŒã ã¬ãŒããžã®åœ±é¿ã«æ°ä»ããªãããã«ãã©ã³ãã¹ã±ãŒãã®ãµã€ãºã¯512x512ãŠããã以äžã§ãªããã°ãªããŸããã ããã«ãé«ããããã®åäžã®ç²ŸåºŠã¯ãèŠèŠçãªå質ã«é¢ããŠã¯æãå°è±¡çãªçµæã§ã¯ãããŸããã§ããã 颚æ¯ã¯è§åŒµã£ãŠãããå Žæã«ãã£ãŠæ²ãã£ãŠãããããé«ããããã®ç²ŸåºŠã2åã«ããå¿ èŠããããŸããã
äžè¬çã«ãç©äºã¯ããŸãè¯ããããŸããã§ããã
ã¹ãŒããŒãã¬ã€ã³-ã³ã³ã»ãããšçè«
: Super Terrain. .
ãã®åŸã次ã®ãããªèããç§ã蚪ãå§ããŸãããã1ã€ã®å€§ããªæ¯èŠ³ãäœãããšã¯ã§ãããå€åœ¢ã®ããã©ãŒãã³ã¹ããŸã ååã«ããã®ã§ãå°ããªãã®ãããããäœã£ãŠäžŠã¹ãŠã¿ãŸãããïŒ ãMinecraftã®ãã£ã³ã¯ã¯ã©ãã§ããïŒããšããèãã¯æªããããŸããã§ããããããã€ãã®åé¡ããããŸããã
- ããã£ã³ã¯ãã®ãµã€ãºãéžæããæ¹æ³
- ãã£ã³ã¯ã®ãžã§ã€ã³ãã«ç®ç«ã€ç¶ãç®ããªãããšã確èªããæ¹æ³
- é£æ¥ããchunk'ovã®ãžã§ã€ã³ãã§çºçããå€åœ¢ãé©çšããæ¹æ³
æåã®åé¡
æåã®åé¡ã¯ããªãäºçŽ°ãªãã®ã§ããã256x256ã®ãã£ã³ã¯ã®ãµã€ãºãå粟床ã§éžæããŸããïŒé«ããããã®è§£å床= 513ïŒã ãã®ã»ããã¢ããã¯ããã·ã³ã®ããã©ãŒãã³ã¹ã®åé¡ãåŒãèµ·ãããŸããã§ããã ããããå°æ¥çã«ã¯ãã£ã³ã¯ã®ãµã€ãºãåæ€èšããå¿ èŠããããŸãããçŸåšã®æ®µéã§ã¯ããã®ãããªãœãªã¥ãŒã·ã§ã³ã¯ç§ã«é©ããŠããŸãã
第äºã®åé¡
2çªç®ã®åé¡ã«é¢ããŠã¯ã2ã€ã®ã³ã³ããŒãã³ãããããŸããã æåã®æ¹æ³ã¯ãæããã«ãé£æ¥ãããã£ã³ã¯ã®é«ããããã®é£æ¥ããããã¯ã»ã«ãã®é«ããæããããšã§ãã ãã®åé¡ã®è§£æ±ºäžã«ãé«ããããã®è§£å床ã2ã®ã¹ãä¹+ 1ã§ããçç±ãç解ããŸãããå³ã§èª¬æããŸãã
æããã«ãé£æ¥ãã颚æ¯ã®é«ãã®å¹³çãç¶æããããã«ãæåã®é¢šæ¯ã®é«ããããã®ãæåŸã®ããã¯ã»ã«ã¯ã次ã®ãæåã®ããã¯ã»ã«ãšé«ããçãããªããã°ãªããŸããã
æããã«ããã¹ãŒããŒãã¬ã€ã³ã-ããã¯ãUnity Terrain'ovã®ãããªãã¯ã¹ã§ããããã€ãããããšå€åœ¢ãé©çšããã¡ã«ããºã ã«ãã£ãŠçµåãããŠããŸãã
ã©ã³ãã¹ã±ãŒããçµåããããã®ã³ãŒãã®å®è£ ãå®äºããåŸïŒå°ããªãµã€ãºã®ããŒã«ã«ããã©ã¡ãŒã·ã§ã³ã®äœ¿çšã¯åŸã§æ®ãããŸãã-ãã¬ã€ã³ãããªãã¯ã¹ãäœæããæšé«ãããã®åæåæåã®ããã®ã¡ã«ããºã ãéçºããå¿ èŠããããŸããïŒ ïŒã幞ããªããšã«ç°¡åã«è§£æ±ºãããŸããã åé¡ã¯ããã®ãããªé¢šæ¯ã®ãçµã¿åãããã®ããã«ãããããå ±æãããé£æ¥ããŠããããšãUnityã«èª¬æããå¿ èŠãããããšã§ããã ãããè¡ãããã«ãéçºè ã¯SetHeighborsã¡ãœãããæäŸããŸãã ã ç§ã¯ãŸã ãããã©ã®ããã«æ©èœããããããç解ããŠããŸãããããããªãã§ã¯ã颚æ¯ã®äº€å·®ç¹ã«åœ±ãšæãçžæš¡æ§ã®ã¢ãŒãã£ãã¡ã¯ããçŸããŸãã
第äžã®åé¡
3ã€ã®äžã§æãé¢çœããŠå°é£ãªãã®åé¡ã¯ãå°ãªããšã1é±éã¯äŒæ¯ãäžããŸããã§ããã æåŸã®å®è£ ã«è³ããŸã§ã4ã€ã®ç°ãªãå®è£ ãåé€ããŸãããããã«ã€ããŠèª¬æããŸãã ããã«ãå®è£ ã®1ã€ã®éèŠãªå¶éã«ã€ããŠç°¡åã«èª¬æããŸããããŒã«ã«å€åœ¢ã¯1ãã£ã³ã¯ãã倧ããã§ããªãããšãåæãšããŠããŸãã å€åœ¢ã¯ãŸã æ¥åéšã«ããå¯èœæ§ããããŸãããå€åœ¢ãããªãã¯ã¹ã®åŽé¢ã¯ãã£ã³ã¯ã®é«ããããã®è§£å床ãè¶ ããŠã¯ãªããããããã¯ãã¹ãŠæ£æ¹åœ¢ã§ãªããã°ãªããŸããïŒé«ããããããã£ã³ã¯ãæªã¿èªäœïŒã äžè¬ã«ãããã¯å€§ããªå¶éã§ã¯ãããŸããã倧ããªå€åœ¢ã¯ãããã€ãã®å°ããªå€åœ¢ãé çªã«é©çšããããšã§ååŸã§ããããã§ãã ãçŽè§åºŠãã«é¢ããŠã¯ãããã¯é«ããããã®å¶éã§ãã ã€ãŸãããã®é«ããããã®ã¿ãæ£æ¹åœ¢ã§ããå¿ èŠãããããã®äžã«å ç®ã¢ããªã±ãŒã·ã§ã³ã®ããŒããã»ã¯ã·ã§ã³ãŸãã¯ä¹ç®ã¢ããªã±ãŒã·ã§ã³ã®ãåäžãã»ã¯ã·ã§ã³ãååšããå ŽåããããŸãã
å€åœ¢èªäœã®æ®éçãªé©çšã®ããã®ã¢ã«ãŽãªãºã ã®ã¢ã€ãã¢ã¯æ¬¡ã®ãšããã§ããïŒ
- ããã©ã¡ãŒã·ã§ã³ãã€ããããã9ã€ã®éšåã«åå²ããŸããåéšåã¯ãããã©ã¡ãŒã·ã§ã³ã圱é¿ããå¯èœæ§ããããã£ã³ã¯ããšã«1ã€ã§ãã ãã®ãããäžå€®éšåã¯ãå€åœ¢ã«ãã£ãŠçŽæ¥ãããããããã£ã³ã¯ã®å€åœ¢ãæ åœããåœäºè ã¯ããã£ã³ã¯ã®å·Šãå³ããŸãã¯äž/äžãªã©ãæ åœããŸãã å€åœ¢ã«ãã£ãŠãã£ã³ã¯ãå€æŽãããªãå Žåããã®ã³ã³ããŒãã³ãã¯nullã«çãããªããŸãã
- éšåçãªé«ããããã察å¿ãããã£ã³ã¯ã«é©çšããããéšåçãªé«ãããããnullã®å Žåãå€æŽãç¡èŠããŸãã
ãã®ã¢ãããŒãã«ããããã£ã³ã¯ã®äžå¿ã«äœçœ®ããä»ã®ãã£ã³ã¯ã«åœ±é¿ãäžããªãå€åœ¢ãããã³ãããã®å¢çãŸãã¯ã³ãŒããŒã«ããå€åœ¢ã®ãŠãããŒãµã«ã¢ããªã±ãŒã·ã§ã³ãå¯èœã«ãªããŸãã å€åœ¢ããã£ã³ã¯å¢çã§éå§ãŸãã¯çµäºããå Žåãããã«é£æ¥ãããã¯ã»ã«ã®å¢çãã¯ã»ã«ãå€æŽããå¿ èŠããããããæ£ç¢ºã«9ã€ã®éšåïŒ4ã€ã§ã¯ãªãïŒã«åå²ããå¿ èŠããããŸãã ïŒç®ã«èŠããç¶ãç®ããªãããã«-åé¡çªå·2ã®è§£æ±ºçãåç §ããŠãã ããïŒã
ã¹ãŒããŒãã¬ã€ã³-ç·Žç¿
SuperTerrainãäœæ
2çªç®ã®åé¡ã®äžéšãšããŠãè€æ°ã®Terrain'ovã1ã€ã«çµåããã©ã³ãã¹ã±ãŒãå šäœã«é©åãããã°ããŒãã«ãé«ãããããé©çšã§ããã¡ãµããºã ãéçºãããŸããã
é«ãããããã°ããŒãã«ã«äœ¿çšããå¯èœæ§ãå¿ èŠã ã£ãã®ã¯ãã©ã³ãã¹ã±ãŒããäœæããããã®æç¶ãåã§ããããã®äœ¿çšã«Square-Diamondã¢ã«ãŽãªãºã ã䜿çšãããããã§ãã
äžè¬çã«ãSuperTerrainã®äœæã¯ãããªãã·ã³ãã«ã§çŽæçãªããã»ã¹ã§ãã
ãã£ã¡
/// <summary> /// Compound terrain object. /// </summary> public class SuperTerrain { /// <summary> /// Contains the array of subterrain objects /// </summary> private Terrain[,] subterrains; /// <summary> /// Superterrain detail. The resulting superterrain is 2^detail terrains. /// </summary> /// <value>The detail.</value> public int Detail { get; private set; } /// <summary> /// Parent gameobject to nest created terrains into. /// </summary> /// <value>The parent.</value> public Transform Parent { get; private set; } /// <summary> /// Builds the new terrain object. /// </summary> /// <returns>The new terrain.</returns> private Terrain BuildNewTerrain() { // Using this divisor because of internal workings of the engine. // The resulting terrain is still going to be subterrain size. var divisor = GameplayConstants.SuperTerrainHeightmapResolution / GameplayConstants.SubterrainSize * 2; var terrainData = new TerrainData { size = new Vector3 (GameplayConstants.SubterrainSize / divisor, GameplayConstants.WorldHeight, GameplayConstants.SubterrainSize / divisor), heightmapResolution = GameplayConstants.SuperTerrainHeightmapResolution }; var newTerrain = Terrain.CreateTerrainGameObject(terrainData).GetComponent<Terrain>(); newTerrain.transform.parent = Parent; newTerrain.transform.gameObject.layer = GameplayConstants.TerrainLayer; newTerrain.heightmapPixelError = GameplayConstants.SuperTerrainPixelError; return newTerrain; } /// <summary> /// Initializes the terrain array and moves the terrain transforms to match their position in the array. /// </summary> private void InitializeTerrainArray() { subterrains = new Terrain[Detail, Detail]; for (int x = 0; x < Detail; x++) { for (int y = 0; y < Detail; y++) { subterrains[y, x] = BuildNewTerrain(); subterrains[y, x].transform.Translate(new Vector3(x * GameplayConstants.SubterrainSize, 0f, y * GameplayConstants.SubterrainSize)); } } } /// <summary> /// Initializes a new instance of the <see cref="SuperTerrain"/> class. /// </summary> /// <param name="detail">Superterrain detail. The resultsing superterrain is 2^detail terrains.</param> /// <param name="parent">Parent gameobject to nest created terrains into.</param> public SuperTerrain(int detail, Transform parent) { Detail = detail; Parent = parent; InitializeTerrainArray(); SetNeighbors(); } /// <summary> /// Iterates through the terrain object and sets the neightbours to match LOD settings. /// </summary> private void SetNeighbors() { ForEachSubterrain ((x, y, subterrain) => { subterrain.SetNeighbors(SafeGetTerrain(x - 1, y), SafeGetTerrain(x, y + 1), SafeGetTerrain(x + 1, y), SafeGetTerrain(x, y - 1)); }); } #region [ Array Helpers ] /// <summary> /// Safely retrieves the terrain object from the array. /// </summary> /// <param name="x">The x coordinate.</param> /// <param name="y">The y coordinate.</param> private Terrain SafeGetTerrain(int x, int y) { if (x < 0 || y < 0 || x >= Detail || y >= Detail) return null; return subterrains[y, x]; } /// <summary> /// Iterates over terrain object and executes the given action /// </summary> /// <param name="lambda">Lambda.</param> private void ForEachSubterrain(Action<int, int, Terrain> lambda) { for (int x = 0; x < Detail; x++) { for (int y = 0; y < Detail; y++) { lambda (x, y, SafeGetTerrain(x, y)); } } } #endregion }
å®éãã©ã³ãã¹ã±ãŒãã®äœæã¯ãInitializeTerrainArrayïŒïŒã¡ãœããã§è¡ãããŸãããã®ã¡ãœããã¯ããã¬ã€ã³é åãæ°ããã€ã³ã¹ã¿ã³ã¹ã§æºãããã²ãŒã ã¯ãŒã«ãã®é©åãªå Žæã«ç§»åããŸãã BuildNewTerrainïŒïŒã¡ãœããã¯ã次ã®ã€ã³ã¹ã¿ã³ã¹ãäœæããå¿ èŠãªãã©ã¡ãŒã¿ãŒã§åæåããGameObject'aå ã«ã芪ããé 眮ããŸãïŒäžå¿ èŠãªã²ãŒã ã§ã€ã³ã¹ãã¯ã¿ãŒãæ±æããªãããã«ãSuperTerrainãã£ã³ã¯ãå«ãã·ãŒã³ã§ã²ãŒã ãªããžã§ã¯ããäºåã«äœæãããããšãæ³å®ããŠããŸãïŒãªããžã§ã¯ããšå¿ èŠã«å¿ããŠã¯ãªãŒã³ã¢ãããç°¡çŽ åããŸããïŒ
ããã§ã¯ãã©ã³ãã¹ã±ãŒãã®å¢çã«ããé»ãã¹ãã©ã€ãã®åé¡ã®1ã€ã§ããSetNeighborsïŒïŒã¡ãœãããåŠçãããŸãããã®ã¡ãœããã¯ãäœæãããã©ã³ãã¹ã±ãŒããå埩åŠçããè¿é£ã«é 眮ããŸãã éèŠïŒ TerrainData.SetNeighborsïŒïŒã¡ãœããã¯ãã°ã«ãŒãå ã®ãã¹ãŠã®é¢šæ¯ã«é©çšããå¿ èŠããããŸãã ã€ãŸããã©ã³ãã¹ã±ãŒãAãã©ã³ãã¹ã±ãŒãBã®äžã®é£äººã§ããããšã瀺ããå Žåãã©ã³ãã¹ã±ãŒãBãã©ã³ãã¹ã±ãŒãAã®äžã®é£äººã§ããããšã瀺ãå¿ èŠããããŸãããã®åé·æ§ã¯å®å šã«æ確ã§ã¯ãããŸãããããã®å Žåã®ããã«ãã¡ãœããã®å埩é©çšãå€§å¹ ã«ç°¡çŽ åãããŸãã
äžèšã®ã³ãŒãã«ã¯ã次ã®ã©ã³ãã¹ã±ãŒããäœæãããšãã«é€æ°ã䜿çšãããªã©ãããã€ãã®èå³æ·±ãç¹ããããŸãã æ£çŽã«èšããšããªããããå¿ èŠãªã®ãããããŸãã-éåžžã®æ¹æ³ã§ïŒé€æ°ãªãã§ïŒã©ã³ãã¹ã±ãŒããäœæãããšãééã£ããµã€ãºã®ã©ã³ãã¹ã±ãŒããäœæãããŸãïŒããã¯ãã°ã®å¯èœæ§ããããŸãã ãã®ä¿®æ£ã¯çµéšçã«åŸããããã®ã§ããããŸã 倱æããŠããªãã®ã§ããã®ãŸãŸã«ããŠããããšã«ããŸããã
ãŸãããªã¹ãã®äžéšã«2ã€ã®äžå¯©ãªãã«ããŒã¡ãœãããããããšã«æ°ä»ããããããŸããã å®éãããã¯ãªãã¡ã¯ã¿ãªã³ã°ã®çµæã«ãããŸããïŒããã€ãã®ãªãã¡ã¯ã¿ãªã³ã°ãè¡ãããããŸã å®å šã§ã¯ãªããå€å°å®å®ããããŒãžã§ã³ããªã¹ãããŠããããïŒã ãããã®æ¹æ³ã¯ãããŒã«ã«ããã³ã°ããŒãã«å€åœ¢ãé©çšãããšãã«ããã«äœ¿çšãããŸãã 圌ãã®ååããã圌ããäœãããŠããã®ãç°¡åã«æšæž¬ã§ããŸãã
ã°ããŒãã«æšé«ãããã®é©çš
ã©ã³ãã¹ã±ãŒããäœæãããã®ã§ããã°ããŒãã«æšé«ããããã®äœ¿çšæ¹æ³ã圌ã«æããŸãã ãã®ããã«ãSuperTerrainã¯ä»¥äžãæäŸããŸã
ããã€ãã®æ¹æ³
/// <summary> /// Sets the global heightmap to match the given one. Given heightmap must match the (SubterrainHeightmapResolution * Detail). /// </summary> /// <param name="heightmap">Heightmap to set the heights from.</param> public void SetGlobalHeightmap(float[,] heightmap) { ForEachSubterrain((x, y, subterrain) => { var chunkStartX = x * GameplayConstants.SuperTerrainHeightmapResolution; var chunkStartY = y * GameplayConstants.SuperTerrainHeightmapResolution; var nextChunkStartX = chunkStartX + GameplayConstants.SuperTerrainHeightmapResolution + 1; var nextChunkStartY = chunkStartY + GameplayConstants.SuperTerrainHeightmapResolution + 1; var sumHm = GetSubHeightMap(heightmap, nextChunkStartX, nextChunkStartY, chunkStartX, chunkStartY)); subterrain.terrainData.SetHeights(0, 0, subHm); }); } /// <summary> /// Retrieves the minor heightmap from the entire heightmap array. /// </summary> /// <returns>The minor height map.</returns> /// <param name="heightMap">Major heightmap.</param> /// <param name="Xborder">Xborder.</param> /// <param name="Yborder">Yborder.</param> /// <param name="x">The x coordinate.</param> /// <param name="y">The y coordinate.</param> private float[,] GetSubHeightMap (float[,] heightMap, int Xborder, int Yborder, int x, int y) { if (Xborder == x || Yborder == y || x < 0 || y < 0) return null; var temp = new float[Yborder - y, Xborder - x]; for (int i = x; i < Xborder; i++) { for(int j = y; j < Yborder; j++) { temp[j - y, i - x] = heightMap[j, i]; } } return temp; }
ç§ã¯åæããŸãããã®ã¡ãœããã®ãã¢ã¯ããŸãè¯ãèŠããŸãããããã¹ãŠã説æããããšããŸãã ãããã£ãŠãSetGlobalHeightmapã¡ãœããã®ååã¯ããèªäœãè¡šããŠããŸãã 圌ãè¡ãããšã¯ããã¹ãŠã®ãã£ã³ã¯ïŒããã§ã¯å°äžãšåŒã°ããŸãïŒãå埩åŠçãããã®åº§æšã«å¯Ÿå¿ããé«ããããã®ãã®éšåã«æ£ç¢ºã«é©çšããããšã§ãã ããã§ã¯ãéã®æªãSetHeightsã䜿çšãããŸãããã®ããã©ãŒãã³ã¹ã«ãããããããã¹ãŠã®åé¯ã«è¿œã蟌ãŸããŸãã ã³ãŒããããããããã«ãSuperTerrainHeightmapResolutionå®æ°ã¯ãé«ããããã®1解å床ãš2ã®ã¹ãä¹ïŒåã®ã»ã¯ã·ã§ã³ã§ååšãæ£åœåãããŠããïŒã®éããèæ ®ããŠããŸããã ãã®ååãšæ··åããªãã§ãã ãã-ãã®å®æ°ã¯ãSuperTerrainå šäœã§ã¯ãªãããã£ã³ã¯ã®é«ããããã®è§£å床ãæ ŒçŽããŸãã SuperTerrainã³ãŒãã¯ããŸããŸãªå®æ°ãåºç¯å²ã«äœ¿çšãããããGameplayConstantsã¯ã©ã¹ãããã«çŽ¹ä»ããŸãã ãããããäœãèµ·ãã£ãŠããããããæ確ã«ãªãã§ãããã ãã®ã¯ã©ã¹ããSuperTerrainã«é¢ä¿ã®ãªããã®ããã¹ãŠåé€ããŸããã
GameplayConstants.cs
namespace Habitat.Game { /// <summary> /// Contains the gameplay constants. /// </summary> public static class GameplayConstants { /// <summary> /// The height of the world. Used in terrain raycasting and Superterrain generation. /// </summary> public const float WorldHeight = 512f; /// <summary> /// Number of the "Terrain" layer /// </summary> public const int TerrainLayer = 8; /// <summary> /// Calculated mask for raycasting against the terrain. /// </summary> public const int TerrainLayerMask = 1 << TerrainLayer; /// <summary> /// Superterrain part side size. /// </summary> public const int SubterrainSize = 256; /// <summary> /// Heightmap resolution for the SuperTerrain. /// </summary> public const int SuperTerrainHeightmapResolution = 512; /// <summary> /// Pixel error for the SuperTerrain. /// </summary> public const int SuperTerrainPixelError = 1; } }
GetSubHeightMapã¡ãœããã«é¢ããŠã¯ãããã¯è»¢éããããããªãã¯ã¹ã®äžéšã®äžéšããã€ããŒãããªãã¯ã¹ã«ã³ããŒããå¥ã®ãã«ããŒã§ãã ããã¯ãSetHeightsããããªãã¯ã¹ã®äžéšãé©çšã§ããªãããã§ãã ãã®å¶éã«ããã倧éã®è¿œå ã¡ã¢ãªå²ãåœãŠãçºçããŸãããããã«ã€ããŠã¯äœãã§ããŸããã æ®å¿µãªãããUnityéçºè ã¯ãªã¢ã«ã¿ã€ã ã®ã©ã³ãã¹ã±ãŒãå€æŽã·ããªãªãæäŸããŸããã§ããã
GetSubHeightMapã¡ãœããã¯ãããŒã«ã«ããã©ã¡ãŒã·ã§ã³ãé©çšãããšãã«ããã«äœ¿çšãããŸãããåŸã§ããã«äœ¿çšãããŸãã
å±æã²ãã¿ã®é©çš
å€åœ¢ãé©çšããã«ã¯ãé«ããããã ãã§ãªãã座æšãé©çšæ¹æ³ã寞æ³ãªã©ã®ä»ã®æ å ±ãå¿ èŠã§ãã ãã®ããŒãžã§ã³ã§ã¯ããã¹ãŠã®æ å ±ãTerrainDeformationã¯ã©ã¹ã«ã«ãã»ã«åããããã®ãªã¹ãã衚瀺ãããŸã
ãã¡ãã
namespace Habitat.DynamicTerrain.Deformation { public abstract class TerrainDeformation { /// <summary> /// Height of the deformation in hightmap pixels. /// </summary> public int H { get; private set; } /// <summary> /// Width of the deformation in hightmap pixels. /// </summary> public int W { get; private set; } /// <summary> /// Heightmap matrix object /// </summary> public float[,] Heightmap { get; private set; } /// <summary> /// Initializes a new instance of the <see cref="Habitat.DynamicTerrain.Deformation.TerrainDeformation"/> class. /// </summary> /// <param name="height">Height in heightmap pixels</param> /// <param name="width">Width in heightmap pixels</param> protected TerrainDeformation(int height, int width) { H = height; W = width; Heightmap = new float[height,width]; } /// <summary> /// Initializes a new instance of the <see cref="Habitat.DynamicTerrain.Deformation.TerrainDeformation"/> class. /// </summary> /// <param name="bitmap">Normalized heightmap matrix.</param> protected TerrainDeformation(float[,] bitmap) { Heightmap = bitmap; H = bitmap.GetUpperBound(0); W = bitmap.GetUpperBound(1); } /// <summary> /// Applies deformation to the point. Additive by default. /// </summary> /// <returns>The to point.</returns> /// <param name="currentValue">Current value.</param> /// <param name="newValue">New value.</param> public virtual float ApplyToPoint(float currentValue, float newValue) { return currentValue + newValue; } /// <summary> /// Generates the heightmap matrix based on constructor parameters. /// </summary> public abstract TerrainDeformation Generate(); } }
ãã®ã¯ã©ã¹ã®çžç¶äººãæœè±¡GenerateïŒïŒã¡ãœãããå®è£ ããŠãããšæšæž¬ããã®ã¯ç°¡åã§ãããã®ã¡ãœããã§ã¯ãå€åœ¢ã«é©ãããã€ãããããäœæããããã®ããžãã¯ãèšè¿°ããŸãã TerrainDeformationã«ã¯ãçŸåšã®ã©ã³ãã¹ã±ãŒããžã®é©çšæ¹æ³ã«é¢ããæ å ±ãå«ãŸããŠããŸããããã¯ãä»®æ³ApplyToPointã¡ãœããã«ãã£ãŠæ±ºå®ãããŸãã ããã©ã«ãã§ã¯ãå€åœ¢ãå æ³ãšããŠå®çŸ©ããŠããŸãããã¡ãœããããªãŒããŒããŒãããããšã«ããã2ã€ã®é«ããçµã¿åãããããè€éãªã¡ãœãããå®çŸã§ããŸãã å€åœ¢ãããªãã¯ã¹ã®ãµããããªãã¯ã¹ãžã®åå²ãšã察å¿ãããã£ã³ã¯ãžã®é©çšã«é¢ããŠã¯ããã®ã³ãŒãã¯SuperTerrainã¯ã©ã¹ã«ããã
次ã®ã°ã«ãŒãã®ã¡ãœããïŒ
/// <summary> /// Compound terrain object. /// </summary> public class SuperTerrain { //... ///<summary> ///Resolution of each terrain in the SuperTerrain; ///</summary> private readonly int hmResolution = GameplayConstants.SuperTerrainHeightmapResolution; /// Applies the partial heightmap to a single terrain object. /// </summary> /// <param name="heightmap">Heightmap.</param> /// <param name="chunkX">Terrain x.</param> /// <param name="chunkY">Terrain y.</param> /// <param name="startX">Start x.</param> /// <param name="startY">Start y.</param> /// <param name="type">Deformation type.</param> private void ApplyPartialHeightmap(float[,] heightmap, int chunkX, int chunkY, int startX, int startY, TerrainDeformation td) { if (heightmap == null) return; var current = subterrains [chunkY, chunkX].terrainData.GetHeights( startX, startY, heightmap.GetUpperBound (1) + 1, heightmap.GetUpperBound (0) + 1); for (int x = 0; x <= heightmap.GetUpperBound(1); x++) { for (int y = 0; y <= heightmap.GetUpperBound(0); y++) { current[y, x] = td.ApplyToPoint(current[y, x], heightmap[y, x]); } } subterrains[chunkY, chunkX].terrainData.SetHeights (startX, startY, current); } private int TransformCoordinate (float coordinate) { return Mathf.RoundToInt(coordinate * hmResolution / GameplayConstants.SubterrainSize); } /// <summary> /// Applies the local deformation. /// </summary> /// <param name="deformation">Deformation.</param> /// <param name="x">The x coordinate.</param> /// <param name="y">The y coordinate.</param> public void ApplyDeformation(TerrainDeformation td, float xCoord, float yCoord) { int x = TransformCoordinate (xCoord); int y = TransformCoordinate (yCoord); var chunkX = x / hmResolution; var chunkY = y / hmResolution; ApplyPartialHeightmap(GetBottomLeftSubmap(td, x, y), chunkX - 1, chunkY - 1, hmResolution, hmResolution, td); ApplyPartialHeightmap(GetLeftSubmap(td, x, y), chunkX - 1, chunkY, hmResolution, y % hmResolution, td); ApplyPartialHeightmap(GetTopLeftSubmap(td, x, y), chunkX - 1, chunkY + 1, hmResolution, 0, td); ApplyPartialHeightmap(GetBottomSubmap(td, x, y), chunkX, chunkY - 1, x % hmResolution, hmResolution, td); ApplyPartialHeightmap(GetBottomRightSubmap(td, x, y), chunkX + 1, chunkY - 1, 0, hmResolution, td); ApplyPartialHeightmap(GetMiddleSubmap(td, x, y), chunkX, chunkY, x % hmResolution, y % hmResolution, td); ApplyPartialHeightmap(GetTopSubmap(td, x, y), chunkX, chunkY + 1, x % hmResolution, 0, td); ApplyPartialHeightmap(GetRightSubmap(td, x, y), chunkX + 1, chunkY, 0, y % hmResolution, td); ApplyPartialHeightmap(GetTopRightSubmap(td, x, y), chunkX + 1, chunkY + 1, 0, 0, td); } ///Retrieves the bottom-left part of the deformation (Subheightmap, applied to the bottom ///left chunk of the targetChunk) or null if no such submap has to be applied. ///Covers corner cases private float[,] GetBottomLeftSubmap(TerrainDeformation td, int x, int y) { if (x % hmResolution == 0 && y % hmResolution == 0 && x / hmResolution > 0 && y / hmResolution > 0) { return new float[,] {{ td.Heightmap[0, 0] }}; } return null; } ///Retrieves the left part of the deformation (Subheightmap, applied to the ///left chunk of the targetChunk) or null if no such submap has to be applied. ///Covers edge cases private float[,] GetLeftSubmap(TerrainDeformation td, int x, int y) { if (x % hmResolution == 0 && x / hmResolution > 0) { int endY = Math.Min((y / hmResolution + 1) * hmResolution, y + td.H); return GetSubHeightMap(td.Heightmap, 1, endY - y, 0, 0); } return null; } ///Retrieves the bottom part of the deformation (Subheightmap, applied to the bottom ///chunk of the targetChunk) or null if no such submap has to be applied. ///Covers edge cases private float[,] GetBottomSubmap(TerrainDeformation td, int x, int y) { if (y % hmResolution == 0 && y / hmResolution > 0) { int endX = Math.Min((x / hmResolution + 1) * hmResolution, x + td.W); return GetSubHeightMap(td.Heightmap, endX - x, 1, 0, 0); } return null; } ///Retrieves the top-left part of the deformation (Subheightmap, applied to the top ///left chunk of the targetChunk) or null if no such submap has to be applied. ///Covers split edge cases private float[,] GetTopLeftSubmap(TerrainDeformation td, int x, int y) { if (x % hmResolution == 0 && x / hmResolution > 0) { int startY = (y / hmResolution + 1) * hmResolution; int endY = y + td.H; if (startY > endY) return null; return GetSubHeightMap(td.Heightmap, 1, td.H, 0, startY - y); } return null; } ///Retrieves the bottom-right part of the deformation (Subheightmap, applied to the bottom ///right chunk of the targetChunk) or null if no such submap has to be applied. ///Covers split edge cases private float[,] GetBottomRightSubmap(TerrainDeformation td, int x, int y) { if (y % hmResolution == 0 && y / hmResolution > 0) { int startX = (x / hmResolution + 1) * hmResolution; int endX = x + td.W; if (startX > endX) return null; return GetSubHeightMap(td.Heightmap, td.W, 1, startX - x, 0); } return null; } ///Retrieves the main deformation part. private float[,] GetMiddleSubmap(TerrainDeformation td, int x, int y) { int endX = Math.Min((x / hmResolution + 1) * hmResolution, x + td.W); int endY = Math.Min((y / hmResolution + 1) * hmResolution, y + td.H); return GetSubHeightMap(td.Heightmap, Math.Min(endX - x + 1, td.Heightmap.GetUpperBound(0) + 1), Math.Min(endY - y + 1, td.Heightmap.GetUpperBound(1) + 1), 0, 0); } ///Retrieves the top deformation part or null if none required private float[,] GetTopSubmap(TerrainDeformation td, int x, int y) { int startY = (y / hmResolution + 1) * hmResolution; if (y + td.H < startY) return null; int endX = Math.Min((x / hmResolution + 1) * hmResolution, x + td.W); return GetSubHeightMap(td.Heightmap, Math.Min (endX - x + 1, td.Heightmap.GetUpperBound(0) + 1), td.H, 0, startY - y); } ///Retrieves the left deformation part or null if none required private float[,] GetRightSubmap(TerrainDeformation td, int x, int y) { int startX = (x / hmResolution + 1) * hmResolution; if (x + td.W < startX) return null; int endY = Math.Min((y / hmResolution + 1) * hmResolution, y + td.H); return GetSubHeightMap(td.Heightmap, td.W, Math.Min(endY - y + 1, td.Heightmap.GetUpperBound(1) + 1), startX - x, 0); } ///Retrieves the top-right part of the main deformation. private float[,] GetTopRightSubmap(TerrainDeformation td, int x, int y) { int startX = (x / hmResolution + 1) * hmResolution; int startY = (y / hmResolution + 1) * hmResolution; if (x + td.W < startX || y + td.H < startY) return null; return GetSubHeightMap(td.Heightmap, td.W, td.H, startX - x, startY - y); } }
ããããæ¢ã«æšæž¬ããããã«ããªã¹ãã«ããå¯äžã®ãããªãã¯ã¡ãœãããæãéèŠãªã¡ãœããã§ãã ApplyDeformationïŒïŒã¡ãœããã䜿çšãããšãæå®ããå€åœ¢ãæå®ãã座æšã®å°åœ¢ã«é©çšã§ããŸãããŸããåŒã³åºããããšãå°åœ¢ã®åº§æšãæšé«ãããã®åº§æšã«å€æãããŸãïŒå°åœ¢ã®å¯žæ³ãæšé«ãããã®è§£å床ãšç°ãªãå Žåããããèæ ®ããå¿ èŠããããŸãïŒãå€åœ¢ã®é©çšã«é¢ãããã¹ãŠã®äœæ¥ã¯ãå€åœ¢ãã察å¿ãããã£ã³ã¯ã«é«ããããã®ãã£ã³ã¯ãé©çšãã9ã€ã®ApplyPartialHeightmapåŒã³åºãå ã§è¡ãããŸããåè¿°ããããã«ãå¯èœæ§ã®ãããã¹ãŠã®å¢çããã³è§åºŠã®å Žåãèæ ®ããããã«ã4ã€ã§ã¯ãªããæ£ç¢ºã«9ã€ã®éšåãå¿ èŠã§ãã
GetXXXSubmapïŒïŒã¡ãœãããé¢äžããã®ã¯ãã®åºåã§ã-ããŸããŸãªãã£ã³ã¯ã®å€åœ¢äœçœ®ãšå¢çã®ããŒã¿ã«åºã¥ããŠãå¿ èŠãªå€åœ¢ãã€ããŒãååŸããŸããå€åœ¢ã察å¿ãããã£ã³ã¯ã«åœ±é¿ãåãŒããããããã®åããã€ããŒãé©çšããã¡ãœããïŒApplyPartialHeightmapïŒïŒïŒãå ¥åã§nullãåãåã£ãå Žåãåã¡ãœããã¯nullãè¿ããŸãã
çµæãšçµè«
çµæãšããŠçããã¡ã«ããºã ã¯ãã¡ããçæ³ãšã¯ã»ã©é ãã§ããããã§ã«æ©èœããŠãããããã©ãŒãã³ã¹èšå®ã«é¢ããŠããçšåºŠã®æè»æ§ãå®çŸããããã«éèŠãªå°åœ¢ãã©ã¡ãŒã¿ãŒã調æŽããããšãã§ããŸããäž»ãªæ¹åç¹ã«ã¯æ¬¡ã®ãã®ããããŸãã
- ããŒãã¯ãŒã¯ã¯å¥ã®ããã»ã¹ã§è¡ãããç¹ã«æ¿ããã·ãŒã³ã§ã®ãã¬ãŒã ã¬ãŒããžã®åœ±é¿ã軜æžããŸãã
- ãã£ãã·ã³ã°ãªã©ã«ãããæ¯åã¡ã¢ãªå²ãåœãŠãåãé€ãããšã«ããããã€ããŒåãæé©åããŸãããããŸã§ã®ãšããããã®ãããªãã®ããã£ãã·ã¥ããæ¹æ³ãæ³åããã®ã¯å°é£ã§ãããŸã第äžã«ãæãé »ç¹ãªã±ãŒã¹ã«éå®ããããšãã§ããŸã-ãã£ã³ã¯ã®çãäžã®ããããªå€åœ¢ã
- ã©ã³ãã¹ã±ãŒããžãªã¡ããªã ãã§ãªãããã®ãã¯ã¹ãã£ïŒã¹ãã©ãããããã®å€æŽã«ããå€åœ¢ïŒã«ã圱é¿ãäžããæ©èœãè¿œå ããŸãã
- åäžã®ãã¬ãŒã ã«è€æ°ã®èšå®ãé©çšããããã®æé©åãããšãã°ãããã€ãã®ãããã¡ã«ãã£ã³ã¯ã®ããã©ã¡ãŒã·ã§ã³ãèç©ããããžãã¯ãäœããã®æ¹æ³ã§çµåããŠé©çšããããžãã¯ã®åŠçã®æåŸã«ãè€æ°ã®ããã©ã¡ãŒã·ã§ã³ããã£ããšããŠãããã£ã³ã¯ã§SetHeightsã1ååŒã³åºããŸãã
ã¹ã¯ãªãŒã³ã·ã§ãã
, - :
( , â «» .
chunk' , , :
, chunk.
( , â «» .
chunk' , , :
, chunk.
ãããŠããã¡ããããã¬ã€å¯èœãªãã¢ãžã®ãªã³ã¯ïŒ
Windows
For Linux
ããã€ãã®æ瀺
, RTS, . . , â . , "~" development console. «man» «help», , spawn_crater sv_spawn_animdef. / . , benchmark' ( framerate , ) ( google drive).
: + WASD = . = . Ctrl = .
: + WASD = . = . Ctrl = .