2025

05-17 16:12

找到了个好网站https://itch.io/game-assets/free/tag-pixel-art,刚好用来找资产

05-17 22:23

卧槽,为什么我精灵图都是对的,但在raylib里面就是不人物的脚不接地?
我人物坐标也设置为了脚的位置(每一帧底边中间),但是就是不对

05-17 00:35

找到原因了,是这个精灵图的每一帧中人物并不是贴边,看来要用上PS来切片了。

05-18 9:35

找到一个好软件 Texture Packer 很方便。

05-18 15:35

忙了一天,终于把交互写好了

第一个例子

05-18 23:31

现在对enemy1类进行管理,抽象为一个enemy类,然后继承enemy1类,方便日后添加更多敌人。

05-19 16:35

自己把好不容易把enemy1抽象出来,发现扩展性太差了,还有一大堆bug。还是让Cursor改改吧🤣。我是fvv😭😭。

05-20 00:08

真不容易啊,汇报一下今天的成果:

  • 抽象出了enemy类,并实现了EvilWizard继承类;
  • 修复了敌人攻击玩家的bug(一碰到敌人的攻击box就扣血);
  • 加入了BattleManger这个用于管理战斗的类,并实现了对于普通enemy敌人的攻击判定和EvilWizar的延时判定;

也同时感谢帮我测试找bug的朋友,目前还有一个bug需要被修复:Player的Roll动作在空中被打断的问题

05-20 00:08

今天主要继续添加一个SunnnyMushroom的继承类,SunnyMushroom可以远程吐出有毒气体攻击玩家,并且可以移动。

这一天的展示

2025-05-18存档

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#include "raylib.h"
#include "player.h"
#include "enemy1.h"
#include <iostream>

int main() {
const int screenWidth = 960;
const int screenHeight = 540;

InitWindow(screenWidth, screenHeight, "Player vs Enemy1 Demo");
SetTargetFPS(60);

Player player({ screenWidth / 2.0f - 150, 400 });
player.InitAnimations();
std::vector<Enemy1*> enemies;
enemies.push_back(new Enemy1({ screenWidth / 2.0f + 150, 400 }, &player));
enemies.push_back(new Enemy1({ screenWidth / 2.0f + 250, 400 }, &player));
enemies.push_back(new Enemy1({ screenWidth / 2.0f + 350, 400 }, &player));

bool enemyHitThisAttack = false; // 控制每次攻击只造成一次伤害

while (!WindowShouldClose()) {
float deltaTime = GetFrameTime();

player.Update(deltaTime);
for (auto* enemy : enemies) {
enemy->UpdateAI(player.GetPosition());
enemy->Update(deltaTime);
}

// 玩家发起攻击,检测是否命中敌人
if (player.IsAttacking()) {
Rectangle playerAttackBox = player.GetAttackHitbox();
for (auto* enemy : enemies) {
if (enemy->IsDead()) continue;

Rectangle enemyHitbox = enemy->GetHitbox();
if (CheckCollisionRecs(playerAttackBox, enemyHitbox)) {
if (!enemyHitThisAttack) {
int damage = player.GetAttackDamage();
enemy->TakeDamage(damage);
enemyHitThisAttack = true;
std::cout << "Enemy took " << damage << " damage! HP left: " << enemy->GetHealth() << std::endl;
}
}
}
} else {
enemyHitThisAttack = false;
}

for (auto* enemy : enemies) {
if (enemy->IsAttacking() && !enemy->hasHitPlayer) {
if (CheckCollisionRecs(enemy->GetAttackHitbox(), player.GetHitbox())) {
player.TakeDamage(enemy->GetAttackDamage());
enemy->hasHitPlayer = true;
}
}
}



BeginDrawing();
ClearBackground(BLACK);

player.Draw();
for (auto* enemy : enemies) {
enemy->Draw();

Rectangle enemyHitbox = enemy->GetHitbox();
DrawRectangleLines((int)enemyHitbox.x, (int)enemyHitbox.y, (int)enemyHitbox.width, (int)enemyHitbox.height, BLUE);
}

// 清除死亡动画已完成的敌人
for (int i = (int)enemies.size() - 1; i >= 0; --i) {
if (enemies[i]->GetCurrentState() == EnemyState::Dead &&
enemies[i]->IsAnimationFinished()) {
delete enemies[i];
enemies.erase(enemies.begin() + i);
}
}

Rectangle attackBox = player.GetAttackHitbox();
DrawRectangleLines((int)attackBox.x, (int)attackBox.y, (int)attackBox.width, (int)attackBox.height, RED);



DrawText("Press J for Light Attack", 10, 10, 20, DARKGRAY);
DrawText("Press L for Heavy Attack", 10, 40, 20, DARKGRAY);

EndDrawing();
}

CloseWindow();
return 0;
}

player.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
//
// Created by META on 25-5-17.
//
#ifndef PLAYER_H
#define PLAYER_H

#include "raylib.h"
#include "animation.h"
#include "effect.h"
#include <string>
#include <unordered_map>

enum class PlayerState {
Idle,
Run,
Jump,
JumpFall,
Attack,
Attack2,
Roll,
AttackCrit, // J键连续触发时的暴击攻击
Attack2Crit, // L键连续触发时的暴击攻击
Hit,
Dead
};



class Player {
public:
int maxHealth = 100;
int currentHealth = 100;
static constexpr float GRAVITY = 600.0f;
static constexpr float MOVE_SPEED = 200.0f;
static constexpr float JUMP_FORCE = -400.0f;
static constexpr float ROLL_SPEED = 300.0f;
static constexpr float CRIT_PROBABILITY = 0.4f; // 40% 暴击概率
enum class CritEffectColor {
Yellow,
White,
Green
};
Player(Vector2 startPos);
~Player();

struct HitEffect {
Texture2D texture;
int frameCount = 7;
int currentFrame = 0;
float frameTime = 0.05f;
float timer = 0.0f;
bool active = false;
Vector2 position = {0, 0};
};
HitEffect hitEffect;

void LoadAnimation(PlayerState state, const std::string& path, int frameCount, float frameTime, bool looping);
void InitAnimations();
void LoadCritEffectTextures();
bool IsInvincible() const { return invincibleTimer > 0.0f;} // 查询是否处于无敌状态
void SetInvincible(float duration) { invincibleTimer = duration;} // 设置无敌状态持续时间(秒)
void TakeDamage(int damage);
void Update(float delta);
void Draw();

int attackDamage = 5; // J键普通攻击伤害
int attackCritDamage = 8; // J键暴击伤害
int attack2Damage = 7; // L键普通攻击伤害
int attack2CritDamage = 10; // L键暴击伤害

int GetCurrentAttackDamage() const;

// 攻击检测用接口
bool IsAttacking() const;
bool IsUsingLightAttack() const;
bool IsCritical() const;
bool IsUsingHeavyAttack() const;
int GetAttackDamage() const;
Rectangle GetAttackHitbox() const;

//Hitbox
Vector2 GetPosition() const;
Rectangle GetHitbox() const;


private:
bool isInvincible = false;
float invincibleTimer = 0.0f; //无敌时间
float invincibleDuration = 0.0f;
Vector2 position;
Vector2 velocity;
bool facingRight;
bool onGround;
PlayerState state;

std::unordered_map<PlayerState, Animation> animations;

// 用于暴击判定的攻击键记录
int lastAttackKey = 0;
float lastAttackTime = 0.0f;
bool lastAttackWasCrit = false;

struct CritEffect {
Texture2D texture;
int frameCount = 5;
int currentFrame = 0;
float frameTime = 0.1f;
float timer = 0.0f;
bool active = false;
Vector2 position = {0, 0};
};
CritEffectColor currentEffectColor = CritEffectColor::Yellow;
std::unordered_map<std::string, Texture2D> critEffectTextures;
CritEffect activeCritEffect;

void HandleInput();
void ApplyPhysics(float delta);
void UpdateAnimation(float delta);
void ChangeState(PlayerState newState);
bool IsInterruptible();

// 流血特效
void LoadHitEffectTexture();


// 暴击特效
void TriggerCritEffect(bool isJKey);
void UpdateCritEffect(float delta);
void DrawCritEffect();

};

#endif // PLAYER_H

player.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
//
// Created by META on 25-5-17.
//

#include "player.h"
#include <cmath>

// ===================== 构造与析构 =====================

Player::Player(Vector2 startPos) :
position(startPos),
velocity({0, 0}),
facingRight(true),
state(PlayerState::Idle),
onGround(true) {}

Player::~Player() {
for (auto& [_, anim] : animations) UnloadTexture(anim.texture);
for (auto& [_, tex] : critEffectTextures) UnloadTexture(tex);
UnloadTexture(hitEffect.texture);
}

// ===================== 动画与资源加载 =====================

void Player::LoadAnimation(PlayerState s, const std::string& path, int frameCount, float frameTime, bool looping) {
Animation anim;
anim.texture = LoadTexture(path.c_str());
anim.frameCount = frameCount;
anim.frameTime = frameTime;
anim.frameWidth = anim.texture.width / frameCount;
anim.looping = looping;
animations[s] = anim;
}

void Player::InitAnimations() {
LoadAnimation(PlayerState::Idle, "assets/player/_Idle.png", 10, 0.1f, true);
LoadAnimation(PlayerState::Run, "assets/player/_Run.png", 10, 0.08f, true);
LoadAnimation(PlayerState::Jump, "assets/player/_Jump.png", 3, 0.15f, false);
LoadAnimation(PlayerState::Attack, "assets/player/_Attack.png", 4, 0.06f, false);
LoadAnimation(PlayerState::Attack2, "assets/player/_Attack2.png", 6, 0.06f, false);
LoadAnimation(PlayerState::AttackCrit, "assets/player/_AttackNoMovement.png", 4, 0.12f, false);
LoadAnimation(PlayerState::Attack2Crit, "assets/player/_Attack2NoMovement.png", 6, 0.12f, false);
LoadAnimation(PlayerState::Roll, "assets/player/_Roll.png", 12, 0.03f, false);
LoadAnimation(PlayerState::JumpFall, "assets/player/_Fall.png", 3, 0.15f, true);
LoadAnimation(PlayerState::Hit, "assets/player/_Hit.png", 1, 0.1f, false);
LoadAnimation(PlayerState::Dead, "assets/player/_Death.png", 10, 0.1f, false);

LoadCritEffectTextures();
LoadHitEffectTexture();
}

// ===================== 主循环逻辑 =====================

void Player::Update(float delta) {
HandleInput();
ApplyPhysics(delta);
UpdateAnimation(delta);
UpdateCritEffect(delta);

// 血液特效更新
if (hitEffect.active) {
hitEffect.timer += delta;
if (hitEffect.timer >= hitEffect.frameTime) {
hitEffect.timer = 0.0f;
hitEffect.currentFrame++;
if (hitEffect.currentFrame >= hitEffect.frameCount) {
hitEffect.active = false;
}
}
}
}

void Player::Draw() {
Animation& anim = animations[state];
float frameWidth = (float)anim.frameWidth;
float frameHeight = (float)anim.texture.height;

Rectangle src = {
anim.currentFrame * frameWidth,
0,
facingRight ? frameWidth : -frameWidth,
frameHeight
};

Rectangle dest = {
position.x - frameWidth / 2.0f,
position.y - frameHeight,
frameWidth,
frameHeight
};

// 血条绘制
float barWidth = 200, barHeight = 20;
float hpRatio = (float)currentHealth / maxHealth;
Vector2 barPos = { 20, (float)GetScreenHeight() - 40 };

DrawRectangleV(barPos, {barWidth, barHeight}, DARKGRAY);
DrawRectangleV(barPos, {barWidth * hpRatio, barHeight}, RED);
DrawRectangleLines((int)barPos.x, (int)barPos.y, (int)barWidth, (int)barHeight, WHITE);
DrawText("HP", (int)barPos.x, (int)(barPos.y - 20), 18, WHITE);

std::string hpText = TextFormat("%d / %d", currentHealth, maxHealth);
int textWidth = MeasureText(hpText.c_str(), 16);
DrawText(hpText.c_str(), barPos.x + barWidth / 2 - textWidth / 2, barPos.y + barHeight / 2 - 8, 16, WHITE);

DrawTexturePro(anim.texture, src, dest, {0, 0}, 0.0f, WHITE);

// 调试:角色边框与中心点
// DrawRectangleLines((int)dest.x, (int)dest.y, (int)dest.width, (int)dest.height, GREEN);
// DrawCircleV(position, 4, RED); // 脚底中心点


DrawCritEffect();

// 血液特效绘制
if (hitEffect.active) {
float frameWidth = (float)hitEffect.texture.width / hitEffect.frameCount;
Rectangle src = {
frameWidth * hitEffect.currentFrame,
0,
frameWidth,
(float)hitEffect.texture.height
};
Rectangle dest = {
hitEffect.position.x - frameWidth / 2.0f,
hitEffect.position.y - 10,
frameWidth,
(float)hitEffect.texture.height
};

DrawTexturePro(hitEffect.texture, src, dest, {0, 0}, 0.0f, WHITE);

// 调试无敌帧
// if (isInvincible) {
// DrawRectangleLinesEx(GetHitbox(), 2.0f, GREEN);
// }

}
}

// ===================== 输入处理 =====================

void Player::HandleInput() {
if (!IsInterruptible()) return;

velocity.x = 0;
bool isMoving = false;

if (IsKeyDown(KEY_D)) {
velocity.x = Player::MOVE_SPEED;
facingRight = true;
isMoving = true;
if (onGround && state != PlayerState::Run) ChangeState(PlayerState::Run);
}
if (IsKeyDown(KEY_A)) {
velocity.x = -Player::MOVE_SPEED;
facingRight = false;
isMoving = true;
if (onGround && state != PlayerState::Run) ChangeState(PlayerState::Run);
}

if (IsKeyPressed(KEY_SPACE) && onGround) {
velocity.y = Player::JUMP_FORCE;
onGround = false;
ChangeState(PlayerState::Jump);
return;
}

if (IsKeyPressed(KEY_J)) {
bool isCrit = GetRandomValue(0, 99) < (int)(CRIT_PROBABILITY * 100);
ChangeState(isCrit ? PlayerState::AttackCrit : PlayerState::Attack);
if (isCrit) TriggerCritEffect(false);
return;
}

if (IsKeyPressed(KEY_L)) {
bool isCrit = GetRandomValue(0, 99) < (int)(CRIT_PROBABILITY * 100);
ChangeState(isCrit ? PlayerState::Attack2Crit : PlayerState::Attack2);
if (isCrit) TriggerCritEffect(true);
return;
}

if (IsKeyPressed(KEY_K)) {
ChangeState(PlayerState::Roll);
return;
}

if (!isMoving && onGround && state != PlayerState::Idle)
ChangeState(PlayerState::Idle);
}

// ===================== 物理与碰撞 =====================

void Player::ApplyPhysics(float delta) {
if (!onGround) {
velocity.y += Player::GRAVITY * delta;
position.y += velocity.y * delta;

if (velocity.y > 0 && state != PlayerState::JumpFall)
ChangeState(PlayerState::JumpFall);

if (position.y >= 400) {
position.y = 400;
velocity.y = 0;
onGround = true;
ChangeState(velocity.x != 0 ? PlayerState::Run : PlayerState::Idle);
}
}

if (state == PlayerState::Roll) {
float dir = facingRight ? 1.0f : -1.0f;
position.x += dir * Player::ROLL_SPEED * delta;
} else {
position.x += velocity.x * delta;
}
}

// ===================== 动画播放控制 =====================

void Player::UpdateAnimation(float delta) {
Animation& anim = animations[state];
anim.timer += delta;

if (anim.timer >= anim.frameTime) {
anim.timer = 0;
anim.currentFrame++;
if (anim.currentFrame >= anim.frameCount) {
if (anim.looping) {
anim.currentFrame = 0;
} else {
anim.currentFrame = anim.frameCount - 1;
anim.finished = true;

switch (state) {
case PlayerState::Attack:
case PlayerState::Attack2:
case PlayerState::AttackCrit:
case PlayerState::Attack2Crit:
case PlayerState::Roll:
case PlayerState::Hit:
ChangeState(onGround ? (velocity.x != 0 ? PlayerState::Run : PlayerState::Idle) : PlayerState::JumpFall);
break;
case PlayerState::Dead:
break;
default:
break;
}
}
}
}
// 无敌计时更新
if (isInvincible) {
invincibleTimer += delta;
if (invincibleTimer >= invincibleDuration) {
isInvincible = false;
invincibleTimer = 0.0f;
}
}
}

// ===================== 状态切换与受击 =====================

void Player::ChangeState(PlayerState newState) {
if (state == newState) return;
state = newState;

Animation& anim = animations[state];
anim.currentFrame = 0;
anim.timer = 0;
anim.finished = false;

// 当进入翻滚状态时,设置无敌时间
if (state == PlayerState::Roll) {
isInvincible = true;
invincibleTimer = 0.0f;
invincibleDuration = 1.0f; // 也可以写成 anim.frameCount * anim.frameTime
}
}

bool Player::IsInterruptible() {
Animation& anim = animations[state];
switch (state) {
case PlayerState::Attack:
case PlayerState::Attack2:
case PlayerState::AttackCrit:
case PlayerState::Attack2Crit:
case PlayerState::Roll:
case PlayerState::Hit:
case PlayerState::Dead:
return anim.finished;
default:
return true;
}
}

void Player::TakeDamage(int damage) {
if (state == PlayerState::Dead || isInvincible) return;

currentHealth -= damage;
if (currentHealth <= 0) {
currentHealth = 0;
ChangeState(PlayerState::Dead);
} else {
ChangeState(PlayerState::Hit);

hitEffect.active = true;
hitEffect.currentFrame = 0;
hitEffect.timer = 0.0f;
hitEffect.position = { position.x - 5, position.y - 20 };
}
}


// ===================== 特效处理 =====================

void Player::LoadCritEffectTextures() {
critEffectTextures["_JGreen"] = LoadTexture("assets/effects/sword/_JGreen.png");
critEffectTextures["_JWhite"] = LoadTexture("assets/effects/sword/_JWhite.png");
critEffectTextures["_JYellow"] = LoadTexture("assets/effects/sword/_JYellow.png");
critEffectTextures["_LGreen"] = LoadTexture("assets/effects/sword/_LGreen.png");
critEffectTextures["_LWhite"] = LoadTexture("assets/effects/sword/_LWhite.png");
critEffectTextures["_LYellow"] = LoadTexture("assets/effects/sword/_LYellow.png");
}

void Player::TriggerCritEffect(bool isLKey) {
std::string key = isLKey ? "_L" : "_J";
switch (currentEffectColor) {
case CritEffectColor::Green: key += "Green"; break;
case CritEffectColor::White: key += "White"; break;
case CritEffectColor::Yellow: key += "Yellow"; break;
}

activeCritEffect.texture = critEffectTextures[key];
activeCritEffect.currentFrame = 0;
activeCritEffect.timer = 0.0f;
activeCritEffect.active = true;
activeCritEffect.position = {
position.x + (facingRight ? 40.0f : -40.0f),
position.y - 60.0f
};
}

void Player::UpdateCritEffect(float delta) {
if (!activeCritEffect.active) return;

activeCritEffect.timer += delta;
if (activeCritEffect.timer >= activeCritEffect.frameTime) {
activeCritEffect.timer = 0.0f;
activeCritEffect.currentFrame++;
if (activeCritEffect.currentFrame >= activeCritEffect.frameCount)
activeCritEffect.active = false;
}
}

void Player::DrawCritEffect() {
if (!activeCritEffect.active) return;

float scale = 2.0f;
float frameWidth = (float)activeCritEffect.texture.width / activeCritEffect.frameCount;

Rectangle src = {
frameWidth * activeCritEffect.currentFrame,
0,
facingRight ? frameWidth : -frameWidth,
(float)activeCritEffect.texture.height
};

Rectangle dest = {
activeCritEffect.position.x - frameWidth * scale / 2,
activeCritEffect.position.y,
frameWidth * scale,
(float)activeCritEffect.texture.height * scale
};

DrawTexturePro(activeCritEffect.texture, src, dest, {0, 0}, 0.0f, WHITE);
}

void Player::LoadHitEffectTexture() {
hitEffect.texture = LoadTexture("assets/effects/around/Blood Splat.png");
}


// ===================== 进攻处理 =====================

bool Player::IsAttacking() const {
return state == PlayerState::Attack ||
state == PlayerState::Attack2 ||
state == PlayerState::AttackCrit ||
state == PlayerState::Attack2Crit;
}

bool Player::IsUsingLightAttack() const {
return state == PlayerState::Attack || state == PlayerState::AttackCrit;
}

bool Player::IsCritical() const {
return state == PlayerState::AttackCrit || state == PlayerState::Attack2Crit;
}

Rectangle Player::GetAttackHitbox() const {
float height = 40.0f; // 角色身高
float halfWidth = 20.0f; // 左右宽度的一半
float offsetX = 0.0f;

if (facingRight) {
// 向右时,攻击区域脚底X坐标向右偏移约 15px,左一样
offsetX = 15.0f;
} else {
offsetX = -15.0f;
}

return Rectangle {
position.x + offsetX - halfWidth, // X起点(左上角)
position.y - height, // Y起点,脚底往上40px
halfWidth * 2, // 宽度30px
height // 高度40px
};
}


bool Player::IsUsingHeavyAttack() const {
return state == PlayerState::Attack2 || state == PlayerState::Attack2Crit;
}

int Player::GetAttackDamage() const {
// 普通攻击伤害
const int lightAttackDamage = 5;
const int heavyAttackDamage = 8;

bool isCrit = IsCritical(); // 你已有判断暴击的函数

if (IsUsingLightAttack()) {
return isCrit ? lightAttackDamage * 2 : lightAttackDamage;
}
else if (IsUsingHeavyAttack()) {
return isCrit ? heavyAttackDamage * 2 : heavyAttackDamage;
}
else {
return 0;
}
}

// ===================== 受击处理 =====================

Vector2 Player::GetPosition() const {
return position;
}

Rectangle Player::GetHitbox() const {
float height = 40.0f; // 身高
float halfWidth = 15.0f; // 宽度一半

return Rectangle {
position.x - halfWidth,
position.y - height,
halfWidth * 2,
height
};
}

enemy1.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
//
// Created by META on 25-5-18.
//

#ifndef ENEMY1_H
#define ENEMY1_H

#include "animation.h"
#include "raylib.h"
#include "player.h"
#include <string>
#include <map>

enum class EnemyState {
Idle,
Run,
Jump,
Fall,
Hurt,
Attack,
Attack2,
Dead
};

class Enemy1 {
public:
bool hasHitPlayer = false; // 每次攻击只打一次
int attackDamage = 5; // 普通攻击伤害
int specialAttackDamage = 10; // 特殊攻击伤害
float attackHitDelay = 0.2f; // 攻击0.2秒后判定是否命中
float attackTimer = 0.0f;
Player* playerRef; //玩家的指针,用于追踪

Enemy1(Vector2 spawnPos, Player* playerRef ,int maxHealth = 40);
~Enemy1();

void Update(float deltaTime);
void Draw();

void UpdateAI(Vector2 playerPos); // 自动朝向与攻击决策

Vector2 GetPosition() const;
int GetHealth() const;
bool IsDead() const;
bool IsAnimationFinished() const;

void SetState(EnemyState state);
EnemyState GetCurrentState() const { return currentState; }

void TakeDamage(int damage);
Rectangle GetHitbox() const;
Rectangle GetAttackHitbox() const; // 当前攻击区域(用于命中判定)
bool IsAttacking() const;
int GetAttackDamage() const;


private:
void LoadAnimation(EnemyState state, const std::string& path, int frameCount, float frameTime, bool looping);
void InitAnimations();
void UpdateAnimation(float deltaTime);
void DrawAnimation();
void DrawHealthBar();

std::map<EnemyState, Animation> animations;
EnemyState currentState = EnemyState::Idle;

Vector2 position;
int health;
int maxHealth;

bool facingRight = true; // 当前朝向,决定贴图翻转与攻击方向
};

#endif // ENEMY1_H

enemy1.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
//
// Created by META on 25-5-18.
//

#include "enemy1.h"
#include "raymath.h"

Enemy1::Enemy1(Vector2 spawnPos, Player* player, int maxHealth)
: position(spawnPos), playerRef(player), health(maxHealth), maxHealth(maxHealth) {
InitAnimations();
}

Enemy1::~Enemy1() {
for (auto& [_, anim] : animations) {
UnloadTexture(anim.texture);
}
}

void Enemy1::LoadAnimation(EnemyState state, const std::string& path, int frameCount, float frameTime, bool looping) {
Animation anim;
anim.texture = LoadTexture(path.c_str());
anim.frameCount = frameCount;
anim.frameWidth = anim.texture.width / frameCount;
anim.frameHeight = anim.texture.height;
anim.frameTime = frameTime;
anim.looping = looping;
anim.currentFrame = 0;
anim.timer = 0.0f;
anim.finished = false;
animations[state] = anim;
}

void Enemy1::InitAnimations() {
LoadAnimation(EnemyState::Idle, "assets/enemy/01/_01Idle.png", 8, 0.1f, true);
LoadAnimation(EnemyState::Run, "assets/enemy/01/_01Run.png", 8, 0.1f, true);
LoadAnimation(EnemyState::Jump, "assets/enemy/01/_01Jump.png", 2, 0.15f, false);
LoadAnimation(EnemyState::Fall, "assets/enemy/01/_01Fall.png", 2, 0.15f, false);
LoadAnimation(EnemyState::Hurt, "assets/enemy/01/_01Hurt.png", 3, 0.1f, false);
LoadAnimation(EnemyState::Attack, "assets/enemy/01/_01Attack.png", 8, 0.1f, false);
LoadAnimation(EnemyState::Attack2, "assets/enemy/01/_01Attack2.png", 8, 0.1f, false);
LoadAnimation(EnemyState::Dead, "assets/enemy/01/_01Dead.png", 7, 0.15f, false);
}



void Enemy1::Update(float deltaTime) {
UpdateAnimation(deltaTime);

if (currentState == EnemyState::Hurt && animations[EnemyState::Hurt].finished) {
SetState(EnemyState::Idle);
}

if (IsDead()) {
SetState(EnemyState::Dead);
}
if (IsAttacking()) {
attackTimer += deltaTime;

if (!hasHitPlayer && attackTimer >= attackHitDelay) {
Rectangle attackBox = GetAttackHitbox();
Rectangle playerBox = playerRef->GetHitbox(); // 假设你已通过接口获取玩家

if (CheckCollisionRecs(attackBox, playerBox)) {
playerRef->TakeDamage(GetAttackDamage());
hasHitPlayer = true;
}
}

if (animations[currentState].finished) {
SetState(EnemyState::Idle);
}
}

}

void Enemy1::Draw() {
DrawAnimation();
DrawHealthBar();
}

void Enemy1::UpdateAnimation(float deltaTime) {
auto& anim = animations[currentState];
anim.timer += deltaTime;
if (anim.timer >= anim.frameTime) {
anim.timer = 0.0f;
anim.currentFrame++;
if (anim.currentFrame >= anim.frameCount) {
if (anim.looping) {
anim.currentFrame = 0;
} else {
anim.currentFrame = anim.frameCount - 1;
anim.finished = true;
}
}
}
}

void Enemy1::DrawAnimation() {
Animation& anim = animations[currentState];
int frameWidth = anim.frameWidth;
int frameHeight = anim.frameHeight;

Rectangle source = {
static_cast<float>(anim.currentFrame * anim.frameWidth),
0.0f,
facingRight ? (float)frameWidth : -(float)frameWidth, // 水平翻转
(float)frameHeight
};

Rectangle dest = {
position.x,
position.y,
(float)frameWidth,
(float)frameHeight
};

Vector2 origin = {
frameWidth / 2.0f,
(float)frameHeight
};

DrawTexturePro(anim.texture, source, dest, origin, 0.0f, WHITE);
}

void Enemy1::DrawHealthBar() {
float barWidth = 50.0f;
float barHeight = 6.0f;
float healthPercent = (float)health / (float)maxHealth;

Vector2 barPos = { position.x - barWidth / 2.0f, position.y - 60.0f };

DrawRectangleV(barPos, { barWidth, barHeight }, DARKGRAY);
DrawRectangleV(barPos, { barWidth * healthPercent, barHeight }, RED);
DrawRectangleLines((int)barPos.x, (int)barPos.y, (int)barWidth, (int)barHeight, BLACK);
}

void Enemy1::TakeDamage(int damage) {
if (IsDead()) return;

health -= damage;
if (health < 0) health = 0;

SetState(IsDead() ? EnemyState::Dead : EnemyState::Hurt);
}

Vector2 Enemy1::GetPosition() const {
return position;
}

int Enemy1::GetHealth() const {
return health;
}

bool Enemy1::IsDead() const {
return health <= 0;
}

void Enemy1::SetState(EnemyState newState) {
if (currentState != newState) {
currentState = newState;
Animation& anim = animations[currentState];
anim.currentFrame = 0;
anim.timer = 0.0f;
anim.finished = false;

// 如果是攻击状态,重置攻击标志
if (currentState == EnemyState::Attack || currentState == EnemyState::Attack2) {
hasHitPlayer = false;
}
}
if (newState == EnemyState::Attack || newState == EnemyState::Attack2) {
hasHitPlayer = false;
attackTimer = 0.0f;
}

}


Rectangle Enemy1::GetHitbox() const {
float width = 23.0f;
float height = 55.0f;

return {
position.x - width / 2.0f,
position.y - height,
width,
height
};
}

Rectangle Enemy1::GetAttackHitbox() const {
float width = 100.0f;
float height = 90.0f;
float offsetX = facingRight ? 0.0f : -width;

return {
position.x + offsetX - width / 2.0f,
position.y - height,
width,
height
};
}

void Enemy1::UpdateAI(Vector2 playerPos) {
if (IsDead()) return;

// 计算与玩家的水平距离
float distance = playerPos.x - position.x;

// 设置朝向
facingRight = distance > 0;

// 如果正在攻击、受伤、死亡,不能AI控制
if (currentState == EnemyState::Attack ||
currentState == EnemyState::Attack2 ||
currentState == EnemyState::Hurt ||
currentState == EnemyState::Dead) {
return;
}

// 攻击判定范围
float attackRange = 40.0f;

if (std::abs(distance) < attackRange) {
// 有一定几率切换成普通攻击或特殊攻击
int chance = GetRandomValue(0, 99);
if (chance < 60) {
SetState(EnemyState::Attack); // 普通攻击
} else {
SetState(EnemyState::Attack2); // 特殊攻击
}
} else {
// 向玩家方向移动
float moveSpeed = 60.0f * GetFrameTime(); // 每秒60像素
position.x += (distance > 0 ? moveSpeed : -moveSpeed);
SetState(EnemyState::Run);
}
}


bool Enemy1::IsAttacking() const {
return currentState == EnemyState::Attack || currentState == EnemyState::Attack2;
}

int Enemy1::GetAttackDamage() const {
return currentState == EnemyState::Attack ? attackDamage : specialAttackDamage;
}

bool Enemy1::IsAnimationFinished() const {
auto it = animations.find(currentState);
if (it != animations.end()) {
return it->second.finished;
}
return false;
}