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
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ặcmoveTo
để 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
Backlinks¶
The following pages link to this page:
Created : Mar 13, 2022
Recent Posts
- 2024-11-02: BUỔI 10 - Phân tích thị trường
- 2024-11-02: BUỔI 11 - Phân tích thị trường
- 2024-11-02: BUỔI 12 - Phân tích sóng tăng
- 2024-11-02: BUỔI 13 - Phân tích hỏi đáp
- 2024-11-02: BUỔI 14 - Yếu tố kiểm soát
- 2024-11-02: BUỔI 15 - Hỏi đáp
- 2024-11-01: BUỔI 6 - Ôn lại và bổ sung
- 2024-11-01: BUỔI 7 - Chiến thuật Trend
- 2024-11-01: BUỔI 8 - Công thức điểm vào lệnh
- 2024-11-01: K2023 - BUỔI 9 - Quy trình vào lệnh