Skip to content

Esenthel-First-Try

Game đầu tiên này tôi sẽ sử dụng dàn ý game từ sách C++ của Esenthel-Book-Basics để làm cho nhanh. Ý tưởng game: mô tả sự tương tác của các sinh vật

  • 1 Player
  • Nhiều loại animal
  • Thực vật
  • Landscape

1 Landscape

Hiện tại landscape tức là Heighmap trong Esenthel thì tôi dùng Template từ Esenthel-Book-Basics

Để load World Map thì sử dụng Game class của Esenthel. Lưu ý cần phải Load thêm các objects cũng như khai báo chúng để có thể sử dụng trong Game

bool GameInit()
{
   Game.World.setObjType(Items     , OBJ_ITEM       )
             .setObjType(AIs       , OBJ_CHR        )
             .setObjType(Players   , OBJ_PLAYER     )
             .setObjType(LightBulbs, OBJ_LIGHT_POINT)
             .New(UID(912913129, 1179123280, 216125083, 1230476798));
   if(Game.World.settings().environment)Game.World.settings().environment->set();
   return true;
}

Ví dụ như setObjType(Items, OBJ_ITEM) có ý nghĩa là một container tên là Items chứa rất nhiều variables/objects có cùng một type là class Item, đặc điểm chung của class Item này được tạo bởi OBJ_ITEM.

Khi World map được tạo và khai báo với các Objects này thì nó sẽ hiểu được trong game có tồn tại chúng. Nếu không khai báo chúng thì KHÔNG THỂ TẠO BẰNG CODE như sử dụng các if, loop để tạo chúng cho game.

**Dự định: **

  • Nhiều loại thực vật phát triển phụ thuộc và ánh sáng, dinh dưỡng, nước
  • Bị tiêu thụ bởi các sinh vật khác

2 Player

Player không phải là thành phần quan trọng nhất trong game này, tuy nhiên cần có nó để dễ dàng khám phá Game, cũng như có thể tạo Minimap cho game.

Các thành phần cần có cho Player

  • Tạo một class Player dựa trên class Chr của Esenthel
  • Trong update() cho phép sử dụng WASD để di chuyển Player

Cách sử dụng các Animation từ Blender (file FBX) cho Esenthel như sau:

  • Sử dụng mặc định như Walk, Run từ Esenthel
  • Sử dụng custom animation tìm được theo findParam
virtual void setParams()
   {
      super.setParams();
      if(base)
      {
         if(Param *p=base->findParam("anim aim left" ))sac_aim_l=skel.getSkelAnim(p.asID());
         if(Param *p=base->findParam("anim aim right"))sac_aim_r=skel.getSkelAnim(p.asID());
      }
   }

2.1 Camera mode

Nếu trong game có Player thì phải tạo thêm một class Camera để xác định tầm quan sát trong game, có thể là First Person hay Third Person, hay từ trên nhìn xuống.

/******************************************************************************

   Here we'll present how to properly use different camera modes

/******************************************************************************/
// Define viewing modes:
enum VIEW_MODE // Viewing Mode
{
   VIEW_FPP, // First Person
   VIEW_TPP, // Third Person
   VIEW_ISO, // Isometric
   VIEW_NUM, // number of view modes
}
VIEW_MODE View=VIEW_TPP; // current VIEW_MODE
/******************************************************************************/
class Player : Game.Chr
{
   bool update()
   {
      if(action)
      {
         if(Kb.b(KB_W) || Kb.b(KB_S) || Kb.b(KB_A) || Kb.b(KB_D) || Kb.b(KB_Q) || Kb.b(KB_E))actionBreak();
      }

      if(!action)
      {
         // turn & move
         input.turn.x=Kb.b(KB_Q)-Kb.b(KB_E);
         input.turn.y=Kb.b(KB_T)-Kb.b(KB_G);
         input.move.x=Kb.b(KB_D)-Kb.b(KB_A);
         input.move.z=Kb.b(KB_W)-Kb.b(KB_S);
         input.move.y=Kb.b(KB_SPACE)-Kb.b(KB_LSHIFT);

         // dodge, crouch, walk, jump
         input.dodge = Kb.bd(KB_D)-Kb.bd(KB_A);
         input.crouch= Kb.b (KB_LSHIFT);
         input.walk  = Kb.b (KB_LCTRL );
         input.jump  =(Kb.bp(KB_SPACE ) ? 3.5 : 0);

         // mouse turn
         if(View!=VIEW_ISO) // don't use mouse turning when in Isometric mode
         {
            Flt max=DegToRad(900)*Time.d();
            angle.x-=Mid(Ms.d().x*1.7, -max, max);
            angle.y+=Mid(Ms.d().y*1.7, -max, max);
         }
      }

      return super.update();
   }

   virtual uint drawPrepare()
   {
      uint draw_mask=0xFFFFFFFF; // set all groups enabled by default
      bool hide_head=(View==VIEW_FPP && mesh); // disable drawing head when we're in FPP mode
      if(hide_head)FlagDisable(draw_mask, IndexToFlag(DG_CHR_HEAD)); // clear head draw group flag
      SetDrawMask(draw_mask); // set draw mask
      uint modes=super.drawPrepare(); // call default drawing after setting the mask
      SetDrawMask(); // reset default mask
      return modes;
   }
}
/******************************************************************************/
Game.ObjMap<Game.Item> Items;
Game.ObjMap<Player   > Players;
/******************************************************************************/

bool Init()
{
   Physics.create();

   Game.World.activeRange(D.viewRange())
             .setObjType (Players, OBJ_CHR)
             .New        (UID(4053788456, 1284500709, 3533893555, 3086486877));
   if(Game.World.settings().environment)Game.World.settings().environment->set();

   return true;
}
/******************************************************************************/
void Shut()
{
   Game.World.del();
}
/******************************************************************************/
void UpdateCamera()
{
   // set next camera mode when Tab pressed
   if(Kb.bp(KB_TAB))
   {
      View=VIEW_MODE((View+1)%VIEW_NUM);

      if(View==VIEW_ISO) // when set to isometric view
      {
         Cam.dist =   10; // set bigger camera distance at start
         Cam.pitch=-PI_4; // set starting camera pitch angle
      }
   }

   // setup the camera
   if(Players.elms()) // if we have at least one player
   {
      Cam.updateBegin();

      // set camera depending on current view mode
      switch(View)
      {
         case VIEW_FPP:
         {
          C OrientM *head=Players[0].skel.getSlot("head"); // obtain player "head" skeleton slot (this was created in Object Editor)
            Cam.setPosDir(head.pos, head.dir, head.perp); // set camera from 'head' position, direction and perpendicular to direction
         }break;

         case VIEW_TPP:
         {
            Cam.dist=Max(1.0, Cam.dist*ScaleFactor(Ms.wheel()*-0.1)); // update camera distance according to mouse wheel
            Cam.setSpherical(Players[0].ctrl.center()+Vec(0, 0.5, 0), Players[0].angle.x, Players[0].angle.y, 0, Cam.dist); // set spherical camera looking at player position with given player angles
         }break;

         default: // VIEW_ISO
         {
            Cam.yaw  -=Ms.d().x; // update camera yaw   angle according to mouse delta x
            Cam.pitch+=Ms.d().y; // update camera pitch angle according to mouse delta y
            Clamp(Cam.pitch, -PI_2, 0); // clamp to possible camera pitch angle
            Cam.dist  =Max(1.0, Cam.dist*ScaleFactor(Ms.wheel()*-0.1)); // update camera distance according to mouse wheel
            Cam.setSpherical(Players[0].pos(), Cam.yaw, Cam.pitch, 0, Cam.dist); // set spherical camera looking at player using camera angles
         }break;
      }

      // after setting camera position and angles:
      Cam.updateEnd().set(); // update camera velocities and activate it
   }else // when no player on the scene
   {
      Cam.transformByMouse(0.1, 100, CAMH_ZOOM|(Ms.b(1)?CAMH_MOVE:CAMH_ROT)); // default camera handling actions
   }
}
bool Update()
{
   if(Kb.bp(KB_ESC))return false;
   Game.World.update(Cam.at);
   UpdateCamera();
   return true;
}

Chú ý tại line có sử dụng skel.getSlot("head")

C OrientM *head=Players[0].skel.getSlot("head"); // obtain player "head" skeleton slot (this was created in Object Editor)

Function này cho phép xác định và sử dụng các bộ phận của Player[0] đầu tiên được add vào từ file FBX. Sử dụng Object Editor của Esenthel để chọn slot name

Tại line hide_head có sử dụng Flag để hide các bộ phận của nhân vật từ file FBX. Khác với Slots thì Groups trong Object Editor cho phép ghép nhiều

 if(hide_head)FlagDisable(draw_mask, IndexToFlag(DG_CHR_HEAD)); // clear head draw group flag

Tạm thời đã tạo được một nhân vật có khả năng di chuyển trong Landscape. Bây giờ cần phải tạo thêm một số animal có khả năng tự di chuyển, tức là cần AI cho chúng.

3 Animals

Có nhiều loại animals trong game này để chúng có thể tương tác với nhau. Gồm 3 nhóm chính:

  • Nhóm con mồi như heo, ngựa: có số lượng lớn và có thể ăn cỏ
  • Nhóm thợ săn như khủng long: có số lượng ít hơn, sinh sản chậm hơn

Cần phải tạo một class animal dựa vào class Chr của Esenthel

  • Các movement của Animals không ảnh hưởng bởi phím WASD
  • Có khả năng tự di chuyển, tạo các functions như findPath hoặc moveTo để Animals tự tìm các location. Ví dụ như thức ăn ở gần đó, hoặc chúng có khả năng ghi nhớ nơi có thức ăn
  • Có khả năng sinh sản nếu Male và Femal Animal đủ tuổi
  • Cần có các chỉ số như Age, Food, Water, Speed

3.1 Class prey - con mồi

Hiện tại tôi vẫn sử dụng các code có sẵn của Esenthel cho AI, cụ thể là từ game Blood, vì trong game này có AI, có khả năng theo đuổi nhân vật.
Các functions cần có

  • Create(), tự tạo theo thời gian
  • findPath(), đi theo một object nào đó, ví dụ như Tree hay Water.
/******************************************************************************/
class AI : Chr
{
   flt            time_to_random_animation=Random.f(10, 15),
                  time_to_random_move     =Random.f( 5, 10),
                  time_to_sense=0,
                  time_to_update_action=0,
                  time_to_attack=0;
   Memc<Motion>     idle_motions, // container for idle   motion animations
                  attack_motions; // container for attack motion animations
   Reference<Chr> target;         // target enemy

   // manage
   virtual void setParams()
   {
      super.setParams();
      if(base)
      {
         if(Param *p=base->findParam("speed"       ))speed       =p.asFlt ();
         if(Param *p=base->findParam("anim speed"  ))anim.speed  =p.asFlt ();
         if(Param *p=base->findParam("move walking"))move_walking=p.asBool();
      }
   }

   // get
   bool inAttackRange(Chr &chr) {return Dist(pos(), chr.pos()) <= ctrl.radius()+chr.ctrl.radius()+0.6;}

   // operations
   virtual void attack(Chr &target)
   {
      T.target=target;
      time_to_update_action=0;
   }
   virtual void hit()
   {
      Vec dir; CosSin(dir.x, dir.z, angle.x+PI_2); dir.y=0; // get AI looking direction

      // check all the players
      REPA(Players)
      {
         Player &plr=Players[i];
         if(inAttackRange(plr))
            if(AbsAngleBetween(dir, plr.pos()-pos())<=PI_3) // 60 degrees
               plr.addDamage(Random.f(5, 8), this);
      }
   }
   virtual void moveTo(C Vec &pos)
   {
      // first try finding path directly to 'pos' location
      // if it fails try finding path to positions near 'pos'

      if(!actionMoveTo(pos))
      {
         for(Int r= 1; r<=3; r++) // radius
         for(Int x=-1; x<=1; x++) // x offset
         for(Int z=-1; z<=1; z++) // z offset
            if(x || z)
               if(actionMoveTo(pos+Vec(x, 0, z)*(r*0.6)))return;
      }
   }
   virtual void animate()
   {
      super.animate();

      REPA(  idle_motions)skel.animateReplace(  idle_motions[i]); // apply idle   animations
      REPA(attack_motions)skel.animateReplace(attack_motions[i]); // apply attack animations
   }
   virtual bool update()
   {
      if(super.update())
      {
         if(alive())
         {
            // play random idle animation
            if((time_to_random_animation-=Time.d())<=0)
            {
               time_to_random_animation=Random.f(10, 15);
               if(base)if(Param *roar=base->findParam("anim roar"))if(roar.asID().valid())idle_motions.New().set(skel, roar.asID());
            }

            // sense if player is nearby
            if(!target.valid())
            {
               if((time_to_sense-=Time.d())<=0)
               {
                  if(Players.elms() && Players[0].alive() && Dist(pos(), Players[0].pos())<=24)attack(Players[0]);
                  time_to_sense=Random.f(1, 2);
               }
            }

            if(target.valid())
            {
               if(!target().alive())
               {
                  target.clear();
                  actionBreak();
               }else
               {
                  bool in_range=inAttackRange(target()); // detect if target is in attack range

                  // update moving path in case the target changes location
                  if((time_to_update_action-=Time.d())<=0 && !in_range)
                  {
                     moveTo(target().pos());
                     time_to_update_action=Random.f(0.05, 0.1);
                  }

                  // if in attack range then rotate to face the target
                  if(in_range)
                  {
                     actionBreak(); // stop moving any further

                     flt angle_delta    =AngleDelta(angle.x+PI_2, Angle(target().pos().xz() - pos().xz())), // calculate angle delta between current look angle and target angle
                         max_frame_delta=Time.d()*DegToRad(270); // 270 degrees per second, this is the maximum angle delta limit to change per frame

                     Clamp(angle_delta, -max_frame_delta, max_frame_delta);
                     angle.x+=angle_delta;
                  }

                  // if in attack range then attack
                  if((time_to_attack-=Time.d())<=0 && in_range)
                  {
                     time_to_attack=Random.f(1, 1.5);
                     if(base)if(Param *attack=base->findParam("anim attack"))if(attack.asID().valid())attack_motions.New().set(skel, attack.asID());
                  }
               }
            }else
            {
               if((time_to_random_move-=Time.d())<=0)
               {
                  if(!action)
                  {
                     actionMoveTo(pos()+Random.vec(-3, 3));
                     time_to_random_move=Random.f(3, 10);
                  }
               }
            }

            // update all motion animations
            REPA(idle_motions)
            {
               if(!idle_motions[i].updateAuto(1, 1, 1))idle_motions.remove(i); // if finished playing then remove
            }

            REPA(attack_motions)
            {
               Motion &motion=attack_motions[i];
               if( motion.eventOccurred("hit"))hit();                   // if "hit" event occured (event's are set in Mesh Editor tool - Animation mode)
               if(!motion.updateAuto(3, 3, 1))attack_motions.remove(i); // if finished playing then remove
            }
         }

         return true;
      }
      return false;
   }

   // io
   virtual bool save(File &f)
   {
      if(super.save(f))
      {
         f<<time_to_random_animation<<time_to_random_move<<time_to_sense<<time_to_update_action<<time_to_attack;
         if(        target.save(f))
         if(  idle_motions.save(f))
         if(attack_motions.save(f))
            return f.ok();
      }
      return false;
   }
   virtual bool load(File &f)
   {
      if(super.load(f))
      {
         f>>time_to_random_animation>>time_to_random_move>>time_to_sense>>time_to_update_action>>time_to_attack;
         if(        target.load(f))
         if(  idle_motions.load(f, skel))
         if(attack_motions.load(f, skel))
            if(f.ok())return true;
      }
      return false;
   }

   virtual void linkReferences() {target.link(Game.World);}
}
/******************************************************************************/

Có một đoạn liên quan đến Mesh Tool và Animation

REPA(attack_motions)
            {
               Motion &motion=attack_motions[i];
               if( motion.eventOccurred("hit"))hit();                   // if "hit" event occured (event's are set in Mesh Editor tool - Animation mode)
               if(!motion.updateAuto(3, 3, 1))attack_motions.remove(i); // if finished playing then remove
            }

Tôi không thể tìm được “hit” trong Animation mode và Mesh Tool…

Có lẽ tôi sẽ tạm dừng sử dụng Esenthel vì các code C++ trong Esenthel thật sự khó hiểu cho tôi trong giai đoạn hiện tại.

4 Prepare data

Add some elements: Drag and drop from folder or use from other Esenthel Project. Keep Ctrl and select several objects then Ctrl T to import to the current project

  • Materials: Grass
  • Objects: Animation, Warrior
  • Textures: Sun

5 Righ click –> New world

Double click to World Map

Then Drag the Grass material (or other) to the material in World to paint the Map

Use Right mouse to add color, or left mouse to remove

6 Drop the character to the world map (or terain)

7 Create code in application

Copy the main code from Tutorial “Camera Modes”, it programs how the character run in the world.

Note: The world UID need to be update before run, you need to remove

Just remove UID() and then drag and drog the World in World Folder to New()

8 Create the Environment in World folder

  • Drag and drop it to World Terrain Map
  • Khi này thì sẽ nhìn được bầu trời

  • Double click vào Environment sẽ cho phép bổ sung bầu trời, mây, nắng
  • Drag the sun texture to the Image in Sun function

9 Add heightmap to terrain

I really like to use the real world map, so I tried to export the heightmap to PNG and import this image to Esenthel Engine

There are several notices:

  • The image size: should be 1009x1009
  • The image should be 16 bit

I will use GIMP to convert the image within these parameters

  • But finally I found that I should create a terrain in Blender first and then export to Esenthel, it will be better. An idea from Blender terrain to Unity
  • For the first idea I just add the height map from this website https://heightmap.skydark.pl/

-After export 1 heightmap in PNG, 16 bit grayscale then drag and drop to Esenthel Engine (EE)

  • Finally, I will have a terrain with heightmap like this

10 Add water

  • Tạm thời đã có nền, nhưng cần phải có nước và thực vật (thức ăn) cho các animal

  • Add “New Water Material”

  • Kết quả sau khi add lake và river như sau. Chú ý khi add lake thì phải cho các point nằm trong vùng trũng của hố thì mới xuất hiện nước được

The following pages link to this page:



Created : Mar 13, 2022