人物
This commit is contained in:
BIN
Game/.gitignore
vendored
Normal file
BIN
Game/.gitignore
vendored
Normal file
Binary file not shown.
25
Game/README.md
Normal file
25
Game/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# 有关本项目的说明
|
||||
|
||||
本项目为AVCEIT2025届软件技术专业软件241班Java实训小组项目
|
||||
This project is about Java Project Training 2025, Major.Software Technology
|
||||
|
||||
### 本项目目前需要做的
|
||||
|
||||
- 实现游戏主界面,添加素材
|
||||
- 完成游戏的核心功能,即双人格斗
|
||||
- 实现基本的局域网内联机游戏功能
|
||||
- *~~打则死路一条~~*
|
||||
|
||||
### 目前正在做的
|
||||
|
||||
- 编写单个角色并实现其状态 (@wsj)
|
||||
- 将游戏素材整理为精灵图 (@wsj)
|
||||
- 给游戏进行界面类添加BUG (@mloluyu)
|
||||
- 给标题界面类添加BUG (@mloluyu)
|
||||
- 寻找一个合适的数据处理类编写方法
|
||||
- *~~丰矿地打则~~*
|
||||
|
||||
### 参与项目的人
|
||||
|
||||
- 武术家 [@wsj](varia.mloluyu.uno/wsj)
|
||||
- 大货车 [@mloluyu](varia.mloluyu.uno/mloluyu)
|
||||
114
Game/pom.xml
Normal file
114
Game/pom.xml
Normal file
@@ -0,0 +1,114 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>uno.mloluyu</groupId>
|
||||
|
||||
<artifactId>game</artifactId>
|
||||
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<gdx.version>1.12.1</gdx.version>
|
||||
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.badlogicgames.gdx</groupId>
|
||||
<artifactId>gdx</artifactId>
|
||||
<version>1.12.1</version> <!-- 替换为你的 LibGDX 版本 -->
|
||||
</dependency>
|
||||
|
||||
<!-- FreeType 字体扩展 -->
|
||||
<dependency>
|
||||
<groupId>com.badlogicgames.gdx</groupId>
|
||||
<artifactId>gdx-freetype</artifactId>
|
||||
<version>1.12.1</version> <!-- 与 LibGDX 版本保持一致 -->
|
||||
</dependency>
|
||||
|
||||
<!-- 桌面平台的 FreeType 原生库 -->
|
||||
<dependency>
|
||||
<groupId>com.badlogicgames.gdx</groupId>
|
||||
<artifactId>gdx-freetype-platform</artifactId>
|
||||
<version>1.12.1</version>
|
||||
<classifier>natives-desktop</classifier>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.badlogicgames.gdx</groupId>
|
||||
<artifactId>gdx-backend-lwjgl</artifactId>
|
||||
<version>1.12.1</version> <!-- 使用你的 LibGDX 版本号 -->
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.badlogicgames.gdx</groupId>
|
||||
<artifactId>gdx-platform</artifactId>
|
||||
<version>1.12.1</version>
|
||||
<classifier>natives-desktop</classifier>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.badlogicgames.gdx</groupId>
|
||||
<artifactId>gdx</artifactId>
|
||||
<version>${gdx.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.badlogicgames.gdx</groupId>
|
||||
<artifactId>gdx-backend-lwjgl3</artifactId>
|
||||
<version>${gdx.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.badlogicgames.gdx</groupId>
|
||||
<artifactId>gdx-platform</artifactId>
|
||||
<version>${gdx.version}</version>
|
||||
<classifier>natives-desktop</classifier>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<!-- 建议使用与LibGDX兼容的JDK版本,11或17比较合适 -->
|
||||
<source>17</source>
|
||||
<target>17</target>
|
||||
<!-- 移除不必要的预览特性,除非确实需要 -->
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<configuration>
|
||||
<!-- 确保这个类路径与你实际的Launcher类位置一致 -->
|
||||
<!-- 例如:如果你的类文件在src/main/java/uno/mloluyu/Launcher.java -->
|
||||
<mainClass>uno.mloluyu.desktop.Launcher</mainClass>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<!-- 添加运行时类路径配置,解决类找不到问题 -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.3.0</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<addClasspath>true</addClasspath>
|
||||
<mainClass>uno.mloluyu.desktop.Launcher</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
||||
</project>
|
||||
11
src/main/java/uno/mloluyu/characters/Action.java
Normal file
11
src/main/java/uno/mloluyu/characters/Action.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package uno.mloluyu.characters;
|
||||
|
||||
public enum Action {
|
||||
IDLE, // 待机状态:角色未执行任何动作,静止或准备中
|
||||
JUMP, // 跳跃状态:角色正在空中跳跃或上升
|
||||
MOVE, // 移动状态:角色正在左右移动
|
||||
ATTACK, // 攻击状态:角色正在执行攻击动作
|
||||
DEFEND, // 防御状态:角色正在格挡或防御中
|
||||
HIT, // 受击状态:角色被攻击命中,进入硬直或受伤动画
|
||||
DEAD // 死亡状态:角色生命值为 0,进入死亡处理
|
||||
}
|
||||
30
src/main/java/uno/mloluyu/characters/AdvancedFighter.java
Normal file
30
src/main/java/uno/mloluyu/characters/AdvancedFighter.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package uno.mloluyu.characters;
|
||||
|
||||
public class AdvancedFighter extends SimpleFighter {
|
||||
|
||||
public AdvancedFighter(String name) {
|
||||
super(name); // 调用父类构造函数
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attack(String attackType) {
|
||||
// 根据攻击类型设置不同攻击力或状态
|
||||
switch (attackType.toLowerCase()) {
|
||||
case "light":
|
||||
changeAction(Action.ATTACK);
|
||||
System.out.println(getName() + " 发起轻攻击!");
|
||||
break;
|
||||
case "heavy":
|
||||
changeAction(Action.ATTACK);
|
||||
System.out.println(getName() + " 发起重攻击!");
|
||||
break;
|
||||
case "special":
|
||||
changeAction(Action.ATTACK);
|
||||
System.out.println(getName() + " 发动特殊技能!");
|
||||
break;
|
||||
default:
|
||||
super.attack(attackType); // 默认调用父类攻击逻辑
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package uno.mloluyu.characters;
|
||||
|
||||
import com.badlogic.gdx.Gdx;
|
||||
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
|
||||
|
||||
/**
|
||||
* Alice角色类,继承自Fighter父类,定义其专属属性和动画
|
||||
*/
|
||||
public class Alice extends Fighter {
|
||||
|
||||
public Alice() {
|
||||
super("Alice", new TextureAtlas(Gdx.files.internal("src/main/resources/character/alice/精灵1.2.atlas")));
|
||||
|
||||
// 设置角色属性
|
||||
speed = 350f; // 更快的移动速度
|
||||
maxHealth = 90; // 较低的生命值
|
||||
health = maxHealth;
|
||||
attackPower = 12; // 中等攻击力
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void loadAnimations() {
|
||||
// 基础动作
|
||||
loadLoopingAnimation(Action.IDLE, "stand/stand", 15);
|
||||
loadLoopingAnimation(Action.WALK, "walkFront/walkFront", 9);
|
||||
loadOneShotAnimation(Action.JUMP, "jump/jump", 8);
|
||||
loadOneShotAnimation(Action.FALL, "hitSpin/hitSpin", 5);
|
||||
|
||||
// 攻击动作
|
||||
loadOneShotAnimation(Action.ATTACK1, "attackAa/attackAa", 6);
|
||||
loadOneShotAnimation(Action.ATTACK2, "attackAb/attackAb", 6);
|
||||
loadOneShotAnimation(Action.ATTACK3, "attackAc/attackAc", 6);
|
||||
loadOneShotAnimation(Action.ATTACK4, "attackAd/attackAd", 6);
|
||||
|
||||
// // 特殊动作(可扩展)
|
||||
// loadOneShotAnimation(Action.SPECIAL1, "special/special1", 6);
|
||||
// loadOneShotAnimation(Action.SPECIAL2, "special/special2", 6);
|
||||
|
||||
// 受击与死亡
|
||||
loadOneShotAnimation(Action.HIT, "hitSpin/hitSpin", 5);
|
||||
// loadOneShotAnimation(Action.DEATH, "death/death", 8);
|
||||
|
||||
// 帧速率调整
|
||||
setFrameDuration(Action.IDLE, 0.04f);
|
||||
setFrameDuration(Action.WALK, 0.08f);
|
||||
setFrameDuration(Action.ATTACK1, 0.07f);
|
||||
setFrameDuration(Action.SPECIAL2, 0.06f);
|
||||
}
|
||||
|
||||
/**
|
||||
* 特定移动状态处理:允许跳跃状态下移动
|
||||
*/
|
||||
@Override
|
||||
protected void handleMoveState() {
|
||||
if (currentAction != Action.ATTACK1 &&
|
||||
currentAction != Action.ATTACK2 &&
|
||||
currentAction != Action.ATTACK3 &&
|
||||
currentAction != Action.ATTACK4 &&
|
||||
currentAction != Action.SPECIAL1 &&
|
||||
currentAction != Action.SPECIAL2 &&
|
||||
currentAction != Action.DEFEND &&
|
||||
currentAction != Action.JUMP &&
|
||||
currentAction != Action.FALL) {
|
||||
changeAction(Action.WALK);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 空中也可以攻击
|
||||
*/
|
||||
@Override
|
||||
protected boolean canAttack() {
|
||||
return super.canAttack() || currentAction == Action.JUMP || currentAction == Action.FALL;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前生命值
|
||||
*/
|
||||
public int getHp() {
|
||||
return health;
|
||||
}
|
||||
}
|
||||
203
src/main/java/uno/mloluyu/characters/SimpleFighter.java
Normal file
203
src/main/java/uno/mloluyu/characters/SimpleFighter.java
Normal file
@@ -0,0 +1,203 @@
|
||||
package uno.mloluyu.characters;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.badlogic.gdx.Gdx;
|
||||
import com.badlogic.gdx.Input;
|
||||
import com.badlogic.gdx.graphics.Color;
|
||||
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
|
||||
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
|
||||
import com.badlogic.gdx.math.Rectangle;
|
||||
|
||||
/**
|
||||
* 简化版角色类,仅包含移动、攻击、受击等基础功能。
|
||||
*/
|
||||
public class SimpleFighter {
|
||||
|
||||
private String name; // 角色名称
|
||||
private final Map<Integer, Float> keyPressDuration = new HashMap<>();
|
||||
|
||||
private Action currentAction = Action.IDLE; // 当前动作状态(待机、攻击、受击等)
|
||||
private float verticalSpeed = 0f; // 垂直速度(可用于跳跃或下落)
|
||||
private boolean isGrounded = true; // 是否在地面上
|
||||
|
||||
private Rectangle hitbox = new Rectangle(0, 0, 64, 128); // 碰撞盒,用于位置和受击判定
|
||||
private Rectangle attackbox = new Rectangle(0, 0, 80, 80); // 攻击盒,用于攻击判定
|
||||
private boolean isFacingRight = true; // 是否面向右侧
|
||||
|
||||
private float speed = 300f; // 移动速度(像素/秒)
|
||||
private int health = 100; // 当前生命值
|
||||
private int attackPower = 10; // 攻击力(暂未使用)
|
||||
|
||||
private boolean isAttacking = false; // 是否正在攻击(攻击状态标志)
|
||||
|
||||
private SimpleFighter fighter; // 添加 fighter 的声明
|
||||
|
||||
private Iterable<Integer> pressedKeys = new HashMap<Integer, Float>().keySet(); // 初始化 pressedKeys
|
||||
|
||||
public SimpleFighter(String name) {
|
||||
this.name = name; // 构造函数,初始化角色名称
|
||||
this.fighter = this; // 初始化 fighter 为当前实例
|
||||
}
|
||||
|
||||
public void update(float deltaTime) {
|
||||
updateAttackbox();
|
||||
|
||||
// 攻击只持续一帧
|
||||
if (isAttacking) {
|
||||
isAttacking = false;
|
||||
changeAction(Action.IDLE);
|
||||
}
|
||||
for (int keycode : pressedKeys) {
|
||||
keyPressDuration.put(keycode, keyPressDuration.getOrDefault(keycode, 0f) + deltaTime); // 更新持续时间
|
||||
fighter.handleInput(keycode, true); // 持续按下的键
|
||||
}
|
||||
// 垂直移动(跳跃或重力)
|
||||
if (!isGrounded) {
|
||||
verticalSpeed -= 980 * deltaTime; // 简单重力模拟
|
||||
hitbox.y += verticalSpeed * deltaTime;
|
||||
|
||||
if (hitbox.y <= 0) {
|
||||
hitbox.y = 0;
|
||||
verticalSpeed = 0;
|
||||
isGrounded = true;
|
||||
changeAction(Action.IDLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void render(SpriteBatch batch, ShapeRenderer shapeRenderer) {
|
||||
batch.end(); // 暂停 SpriteBatch 渲染,切换到 ShapeRenderer
|
||||
System.out.println("人物状态" + currentAction);
|
||||
boolean isAttacking = currentAction == Action.ATTACK;
|
||||
shapeRenderer.begin(ShapeRenderer.ShapeType.Line); // 开始绘制线框
|
||||
|
||||
shapeRenderer.setColor(Color.BLUE); // 设置颜色为蓝色
|
||||
shapeRenderer.rect(hitbox.x, hitbox.y, hitbox.width, hitbox.height); // 绘制碰撞盒
|
||||
|
||||
if (isAttacking) {
|
||||
shapeRenderer.setColor(Color.RED); // 设置颜色为红色
|
||||
shapeRenderer.rect(attackbox.x, attackbox.y, attackbox.width, attackbox.height); // 绘制攻击盒
|
||||
}
|
||||
|
||||
shapeRenderer.end(); // 结束 ShapeRenderer 渲染
|
||||
batch.begin(); // 恢复 SpriteBatch 渲染
|
||||
}
|
||||
|
||||
public void handleInput(int keycode, boolean isPressed, float duration) {
|
||||
// 根据按键和按下状态处理输入行为
|
||||
if (isPressed) {
|
||||
if (keycode == Input.Keys.LEFT || keycode == Input.Keys.A) {
|
||||
move(-1, Gdx.graphics.getDeltaTime()); // 向左移动
|
||||
} else if (keycode == Input.Keys.RIGHT || keycode == Input.Keys.D) {
|
||||
move(1, Gdx.graphics.getDeltaTime()); // 向右移动
|
||||
}
|
||||
if (keycode == Input.Keys.Z || keycode == Input.Keys.J) {
|
||||
attack(""); // 普通攻击
|
||||
} else if (keycode == Input.Keys.X || keycode == Input.Keys.K) {
|
||||
attack(""); // 重攻击(暂未区分)
|
||||
} else if (keycode == Input.Keys.SPACE || keycode == Input.Keys.UP || keycode == Input.Keys.W) {
|
||||
attack(""); // 跳跃(暂未实现跳跃逻辑)
|
||||
} else if (keycode == Input.Keys.SHIFT_LEFT || keycode == Input.Keys.SHIFT_RIGHT) {
|
||||
attack(""); // 防御(暂未实现防御逻辑)
|
||||
}
|
||||
if (keycode == Input.Keys.SPACE || keycode == Input.Keys.UP || keycode == Input.Keys.W) {
|
||||
System.out.println("点击了跳跃");
|
||||
jump();
|
||||
}
|
||||
|
||||
} else {
|
||||
// 松开防御键时恢复待机状态
|
||||
if ((keycode == Input.Keys.SHIFT_LEFT || keycode == Input.Keys.SHIFT_RIGHT) &&
|
||||
getCurrentAction() == Action.DEFEND) {
|
||||
changeAction(Action.IDLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void handleInput(int keycode, boolean isPressed) {
|
||||
handleInput(keycode, isPressed, 0f); // 调用已有方法,补充默认持续时间
|
||||
}
|
||||
|
||||
public void handleRelease(int keycode, float duration) {
|
||||
// 处理按键释放逻辑
|
||||
System.out.println("按键释放: " + keycode + ", 持续时间: " + duration);
|
||||
keyPressDuration.remove(keycode);
|
||||
if (keycode == Input.Keys.LEFT || keycode == Input.Keys.RIGHT || keycode == Input.Keys.A
|
||||
|| keycode == Input.Keys.D) {
|
||||
changeAction(Action.IDLE);
|
||||
}
|
||||
}
|
||||
|
||||
public Action getCurrentAction() {
|
||||
return currentAction; // 获取当前动作状态
|
||||
}
|
||||
|
||||
public void changeAction(Action newAction) {
|
||||
this.currentAction = newAction; // 切换角色动作状态
|
||||
}
|
||||
|
||||
public void jump() {
|
||||
if (isGrounded) {
|
||||
verticalSpeed = 600f;
|
||||
isGrounded = false;
|
||||
changeAction(Action.JUMP);
|
||||
}
|
||||
}
|
||||
|
||||
public void move(float x, float deltaTime) {
|
||||
if (x != 0) {
|
||||
isFacingRight = x > 0;
|
||||
hitbox.x += x * speed * deltaTime;
|
||||
changeAction(Action.MOVE); // 移动时切换为 MOVE 状态
|
||||
} else if (isGrounded && !isAttacking) {
|
||||
changeAction(Action.IDLE); // 停止移动时恢复待机
|
||||
}
|
||||
}
|
||||
|
||||
public void attack(String attackType) {
|
||||
isAttacking = true; // 设置攻击状态
|
||||
changeAction(Action.ATTACK); // 切换为攻击动作
|
||||
}
|
||||
|
||||
public void takeHit(int damage) {
|
||||
health = Math.max(0, health - damage); // 扣除生命值,最小为 0
|
||||
changeAction(health > 0 ? Action.HIT : Action.DEAD); // 根据生命值切换为受击或死亡状态
|
||||
}
|
||||
|
||||
public boolean isAlive() {
|
||||
return health > 0; // 判断角色是否存活
|
||||
}
|
||||
|
||||
public boolean isAttacking() {
|
||||
return isAttacking; // 判断是否处于攻击状态
|
||||
}
|
||||
|
||||
private void updateAttackbox() {
|
||||
// 根据朝向更新攻击盒位置,使其位于角色前方
|
||||
float offsetX = isFacingRight ? hitbox.width - 10 : -attackbox.width + 10;
|
||||
attackbox.setPosition(hitbox.x + offsetX, hitbox.y + 20);
|
||||
}
|
||||
|
||||
// 常用访问器
|
||||
public Rectangle getHitbox() {
|
||||
return hitbox; // 获取碰撞盒
|
||||
}
|
||||
|
||||
public Rectangle getAttackbox() {
|
||||
return attackbox; // 获取攻击盒
|
||||
}
|
||||
|
||||
public int getHealth() {
|
||||
return health; // 获取当前生命值
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name; // 获取角色名称
|
||||
}
|
||||
|
||||
public void setPosition(float x, float y) {
|
||||
hitbox.setPosition(x, y); // 设置角色位置
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package uno.mloluyu.characters.character;
|
||||
|
||||
public enum Action {
|
||||
IDLE, WALK, JUMP, FALL,
|
||||
ATTACK1, ATTACK2, ATTACK3, ATTACK4,
|
||||
HIT, DEFEND,
|
||||
SPECIAL1, SPECIAL2,
|
||||
DEATH
|
||||
}
|
||||
70
src/main/java/uno/mloluyu/characters/character/Alice.java
Normal file
70
src/main/java/uno/mloluyu/characters/character/Alice.java
Normal file
@@ -0,0 +1,70 @@
|
||||
package uno.mloluyu.characters.character;
|
||||
|
||||
import com.badlogic.gdx.Gdx;
|
||||
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
|
||||
|
||||
/**
|
||||
* Alice角色类,继承自Fighter父类,定义其专属属性和动画
|
||||
*/
|
||||
public class Alice extends Fighter {
|
||||
|
||||
private static final String ATLAS_PATH = "src/main/resources/character/alice/精灵1.2.atlas";
|
||||
|
||||
public Alice() {
|
||||
super("Alice", new TextureAtlas(Gdx.files.internal(ATLAS_PATH)));
|
||||
|
||||
speed = 350f;
|
||||
maxHealth = 90;
|
||||
health = maxHealth;
|
||||
attackPower = 12;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void loadAnimations() {
|
||||
animationManager.loadLooping(Action.IDLE, "stand/stand", 15);
|
||||
animationManager.loadLooping(Action.WALK, "walkFront/walkFront", 9);
|
||||
animationManager.loadOneShot(Action.JUMP, "jump/jump", 8);
|
||||
animationManager.loadOneShot(Action.FALL, "hitSpin/hitSpin", 5);
|
||||
|
||||
animationManager.loadOneShot(Action.ATTACK1, "attackAa/attackAa", 6);
|
||||
animationManager.loadOneShot(Action.ATTACK2, "attackAb/attackAb", 6);
|
||||
animationManager.loadOneShot(Action.ATTACK3, "attackAc/attackAc", 6);
|
||||
animationManager.loadOneShot(Action.ATTACK4, "attackAd/attackAd", 6);
|
||||
|
||||
animationManager.loadOneShot(Action.HIT, "hitSpin/hitSpin", 5);
|
||||
// animationManager.loadOneShot(Action.DEATH, "death/death", 8);
|
||||
|
||||
// 可选特殊动作(如资源存在可启用)
|
||||
// animationManager.loadOneShot(Action.SPECIAL1, "special/special1", 6);
|
||||
// animationManager.loadOneShot(Action.SPECIAL2, "special/special2", 6);
|
||||
|
||||
animationManager.setFrameDuration(Action.IDLE, 0.04f);
|
||||
animationManager.setFrameDuration(Action.WALK, 0.08f);
|
||||
animationManager.setFrameDuration(Action.ATTACK1, 0.07f);
|
||||
animationManager.setFrameDuration(Action.SPECIAL2, 0.06f);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleMoveState() {
|
||||
if (currentAction != Action.ATTACK1 &&
|
||||
currentAction != Action.ATTACK2 &&
|
||||
currentAction != Action.ATTACK3 &&
|
||||
currentAction != Action.ATTACK4 &&
|
||||
currentAction != Action.SPECIAL1 &&
|
||||
currentAction != Action.SPECIAL2 &&
|
||||
currentAction != Action.DEFEND &&
|
||||
currentAction != Action.JUMP &&
|
||||
currentAction != Action.FALL) {
|
||||
changeAction(Action.WALK);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean canAttack() {
|
||||
return super.canAttack() || currentAction == Action.JUMP || currentAction == Action.FALL;
|
||||
}
|
||||
|
||||
public int getHp() {
|
||||
return health;
|
||||
}
|
||||
}
|
||||
@@ -1,146 +1,113 @@
|
||||
package uno.mloluyu.characters;
|
||||
package uno.mloluyu.characters.character;
|
||||
|
||||
import com.badlogic.gdx.Gdx;
|
||||
import com.badlogic.gdx.graphics.g2d.*;
|
||||
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
|
||||
import com.badlogic.gdx.math.Rectangle;
|
||||
import com.badlogic.gdx.utils.Array;
|
||||
import com.badlogic.gdx.utils.Disposable;
|
||||
import uno.mloluyu.util.SimpleFormatter;
|
||||
|
||||
import java.util.EnumMap;
|
||||
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
|
||||
|
||||
/**
|
||||
* 抽象类 Fighter,定义所有角色的基础属性与行为。
|
||||
* 包括动画控制、移动、攻击、受击、渲染等核心逻辑。
|
||||
*/
|
||||
public abstract class Fighter implements Disposable {
|
||||
|
||||
public enum Action {
|
||||
IDLE, WALK, JUMP, FALL,
|
||||
ATTACK1, ATTACK2, ATTACK3, ATTACK4,
|
||||
HIT, DEFEND,
|
||||
SPECIAL1, SPECIAL2,
|
||||
DEATH
|
||||
}
|
||||
|
||||
// 默认帧持续时间(秒)
|
||||
protected static final float DEFAULT_FRAME_DURATION = 0.1f;
|
||||
// 默认生命值
|
||||
protected static final int DEFAULT_HEALTH = 100;
|
||||
// 默认移动速度(像素/秒)
|
||||
protected static final float DEFAULT_SPEED = 300f;
|
||||
|
||||
// 角色名称
|
||||
protected String name;
|
||||
// 当前动作状态
|
||||
protected Action currentAction = Action.IDLE;
|
||||
// 当前动作已持续时间
|
||||
protected float stateTime = 0f;
|
||||
// 是否面向右侧
|
||||
protected boolean isFacingRight = true;
|
||||
// 当前动画是否播放完毕
|
||||
protected boolean isAnimationFinished = false;
|
||||
|
||||
protected EnumMap<Action, Animation<TextureRegion>> animations = new EnumMap<>(Action.class);
|
||||
protected EnumMap<Action, Float> frameDurations = new EnumMap<>(Action.class);
|
||||
|
||||
// 碰撞盒(用于位置和受击判定)
|
||||
protected Rectangle hitbox = new Rectangle(0, 0, 64, 128);
|
||||
// 攻击盒(用于攻击判定)
|
||||
protected Rectangle attackbox = new Rectangle(0, 0, 80, 80);
|
||||
|
||||
// 移动速度
|
||||
protected float speed = DEFAULT_SPEED;
|
||||
// 当前生命值
|
||||
protected int health = DEFAULT_HEALTH;
|
||||
// 最大生命值
|
||||
protected int maxHealth = DEFAULT_HEALTH;
|
||||
// 攻击力
|
||||
protected int attackPower = 10;
|
||||
|
||||
protected TextureAtlas atlas;
|
||||
protected float scaleX = 1.0f;
|
||||
protected float scaleY = 1.0f;
|
||||
|
||||
public Fighter() {}
|
||||
// 动画管理器
|
||||
protected FighterAnimationManager animationManager;
|
||||
|
||||
/**
|
||||
* 构造函数,初始化角色名称与动画资源。
|
||||
*
|
||||
* @param name 角色名称
|
||||
* @param atlas 动画图集
|
||||
*/
|
||||
public Fighter(String name, TextureAtlas atlas) {
|
||||
this.name = name;
|
||||
this.atlas = atlas;
|
||||
for (Action action : Action.values()) {
|
||||
frameDurations.put(action, DEFAULT_FRAME_DURATION);
|
||||
}
|
||||
this.animationManager = new FighterAnimationManager(atlas);
|
||||
loadAnimations();
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载角色的所有动画资源,由子类实现。
|
||||
*/
|
||||
protected abstract void loadAnimations();
|
||||
|
||||
protected void loadAnimationFromAtlas(Action action, String regionPrefix, int frameCount, boolean loop) {
|
||||
if (atlas == null)
|
||||
throw new IllegalStateException("TextureAtlas 未初始化!");
|
||||
if (frameCount <= 0)
|
||||
throw new IllegalArgumentException("帧数必须大于0: " + frameCount);
|
||||
|
||||
Array<TextureRegion> frames = new Array<>();
|
||||
for (int i = 0; i < frameCount; i++) {
|
||||
String regionName = regionPrefix + SimpleFormatter.addLeadingZeros(i, 3);
|
||||
TextureRegion region = atlas.findRegion(regionName);
|
||||
if (region == null) {
|
||||
throw new IllegalArgumentException("未找到区域: " + regionName);
|
||||
}
|
||||
frames.add(region);
|
||||
}
|
||||
|
||||
Animation<TextureRegion> animation = new Animation<>(frameDurations.get(action), frames);
|
||||
animation.setPlayMode(loop ? Animation.PlayMode.LOOP : Animation.PlayMode.NORMAL);
|
||||
animations.put(action, animation);
|
||||
}
|
||||
|
||||
protected void loadLoopingAnimation(Action action, String prefix, int count) {
|
||||
loadAnimationFromAtlas(action, prefix, count, true);
|
||||
}
|
||||
|
||||
protected void loadOneShotAnimation(Action action, String prefix, int count) {
|
||||
loadAnimationFromAtlas(action, prefix, count, false);
|
||||
}
|
||||
|
||||
protected void setFrameDuration(Action action, float duration) {
|
||||
frameDurations.put(action, duration);
|
||||
Animation<TextureRegion> anim = animations.get(action);
|
||||
if (anim != null)
|
||||
anim.setFrameDuration(duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 每帧更新角色状态,包括动画播放与碰撞盒更新。
|
||||
*
|
||||
* @param deltaTime 帧间隔时间
|
||||
*/
|
||||
public void update(float deltaTime) {
|
||||
stateTime += deltaTime;
|
||||
Animation<TextureRegion> anim = animations.get(currentAction);
|
||||
if (anim != null) {
|
||||
isAnimationFinished = anim.isAnimationFinished(stateTime);
|
||||
}
|
||||
isAnimationFinished = animationManager.isFinished(currentAction, stateTime);
|
||||
handleAnimationTransitions();
|
||||
updateHitboxes();
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染角色当前帧。
|
||||
*
|
||||
* @param batch 渲染批处理器
|
||||
*/
|
||||
public void render(SpriteBatch batch) {
|
||||
animationManager.render(batch, currentAction, stateTime, hitbox, isFacingRight);
|
||||
}
|
||||
|
||||
/**
|
||||
* 动画播放完毕后的动作切换逻辑。
|
||||
*/
|
||||
protected void handleAnimationTransitions() {
|
||||
if (!isAnimationFinished) return;
|
||||
if (!isAnimationFinished)
|
||||
return;
|
||||
|
||||
switch (currentAction) {
|
||||
case ATTACK1, ATTACK2, ATTACK3, SPECIAL1, SPECIAL2, HIT -> changeAction(Action.IDLE);
|
||||
case JUMP -> changeAction(Action.FALL);
|
||||
default -> {}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void render(SpriteBatch batch) {
|
||||
Animation<TextureRegion> anim = animations.get(currentAction);
|
||||
if (anim == null) {
|
||||
Gdx.app.error("Fighter", "动画未初始化: " + currentAction);
|
||||
return;
|
||||
}
|
||||
|
||||
TextureRegion frame = anim.getKeyFrame(stateTime, anim.getPlayMode() == Animation.PlayMode.LOOP);
|
||||
if (frame == null) {
|
||||
Gdx.app.error("Fighter", "动画帧为空: " + currentAction);
|
||||
return;
|
||||
}
|
||||
|
||||
float frameWidth = frame.getRegionWidth() * scaleX;
|
||||
float frameHeight = frame.getRegionHeight() * scaleY;
|
||||
float drawX = hitbox.x + (hitbox.width - frameWidth) / 2;
|
||||
float drawY = hitbox.y;
|
||||
|
||||
boolean wasFlippedX = frame.isFlipX();
|
||||
frame.flip(!isFacingRight && !wasFlippedX, false);
|
||||
frame.flip(isFacingRight && wasFlippedX, false);
|
||||
|
||||
batch.draw(frame, drawX, drawY, frameWidth / 2, frameHeight / 2, frameWidth, frameHeight, 1f, 1f, 0f);
|
||||
frame.flip(wasFlippedX != frame.isFlipX(), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换角色动作状态。
|
||||
*
|
||||
* @param newAction 新动作
|
||||
* @return 是否成功切换
|
||||
*/
|
||||
public boolean changeAction(Action newAction) {
|
||||
if (isActionUninterruptible(currentAction)) return false;
|
||||
if (isActionUninterruptible(currentAction))
|
||||
return false;
|
||||
if (currentAction != newAction) {
|
||||
currentAction = newAction;
|
||||
stateTime = 0f;
|
||||
@@ -150,15 +117,30 @@ public abstract class Fighter implements Disposable {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断某个动作是否不可打断(如受击或死亡)。
|
||||
*
|
||||
* @param action 动作枚举
|
||||
* @return 是否不可打断
|
||||
*/
|
||||
protected boolean isActionUninterruptible(Action action) {
|
||||
return action == Action.HIT || action == Action.DEATH;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新攻击盒位置(根据角色朝向调整)。
|
||||
*/
|
||||
protected void updateHitboxes() {
|
||||
float offsetX = isFacingRight ? hitbox.width - 10 : -attackbox.width + 10;
|
||||
attackbox.setPosition(hitbox.x + offsetX, hitbox.y + 20);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动角色。
|
||||
*
|
||||
* @param x 水平移动方向(-1左,1右)
|
||||
* @param deltaTime 帧间隔时间
|
||||
*/
|
||||
public void move(float x, float deltaTime) {
|
||||
if (x != 0) {
|
||||
isFacingRight = x > 0;
|
||||
@@ -169,19 +151,29 @@ public abstract class Fighter implements Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动状态下的动作切换逻辑。
|
||||
*/
|
||||
protected void handleMoveState() {
|
||||
if (!isActionUninterruptible(currentAction) &&
|
||||
currentAction != Action.JUMP &&
|
||||
currentAction != Action.FALL &&
|
||||
currentAction != Action.DEFEND &&
|
||||
!currentAction.name().startsWith("ATTACK") &&
|
||||
!currentAction.name().startsWith("SPECIAL")) {
|
||||
currentAction != Action.JUMP &&
|
||||
currentAction != Action.FALL &&
|
||||
currentAction != Action.DEFEND &&
|
||||
!currentAction.name().startsWith("ATTACK") &&
|
||||
!currentAction.name().startsWith("SPECIAL")) {
|
||||
changeAction(Action.WALK);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起攻击动作。
|
||||
*
|
||||
* @param attackType 攻击类型(1~5)
|
||||
* @return 是否成功发起攻击
|
||||
*/
|
||||
public boolean attack(int attackType) {
|
||||
if (!canAttack()) return false;
|
||||
if (!canAttack())
|
||||
return false;
|
||||
|
||||
Action attackAction = switch (attackType) {
|
||||
case 1 -> Action.ATTACK1;
|
||||
@@ -195,25 +187,49 @@ public abstract class Fighter implements Disposable {
|
||||
return attackAction != null && changeAction(attackAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前是否可以攻击。
|
||||
*
|
||||
* @return 是否可攻击
|
||||
*/
|
||||
protected boolean canAttack() {
|
||||
return currentAction == Action.IDLE || currentAction == Action.WALK;
|
||||
}
|
||||
|
||||
/**
|
||||
* 接受伤害。
|
||||
*
|
||||
* @param damage 伤害值
|
||||
*/
|
||||
public void takeHit(int damage) {
|
||||
if (currentAction == Action.DEATH) return;
|
||||
if (currentAction == Action.DEATH)
|
||||
return;
|
||||
|
||||
health = Math.max(0, health - damage);
|
||||
changeAction(health == 0 ? Action.DEATH : Action.HIT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置角色位置。
|
||||
*
|
||||
* @param x 横坐标
|
||||
* @param y 纵坐标
|
||||
*/
|
||||
public void setPosition(float x, float y) {
|
||||
hitbox.setPosition(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置角色朝向。
|
||||
*
|
||||
* @param facingRight 是否面向右
|
||||
*/
|
||||
public void setFacingRight(boolean facingRight) {
|
||||
this.isFacingRight = facingRight;
|
||||
}
|
||||
|
||||
// 以下为常用属性访问器
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
@@ -254,8 +270,18 @@ public abstract class Fighter implements Disposable {
|
||||
return hitbox.y + hitbox.height / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* 帧事件监听器接口,用于在动画播放到某一帧时触发逻辑。
|
||||
*/
|
||||
public interface FrameEventListener {
|
||||
void onFrameEvent(Action action, int frameIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源(如动画图集)。
|
||||
*/
|
||||
@Override
|
||||
public void dispose() {
|
||||
if (atlas != null) atlas.dispose();
|
||||
animationManager.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package uno.mloluyu.characters.character;
|
||||
|
||||
import com.badlogic.gdx.graphics.g2d.*;
|
||||
import com.badlogic.gdx.math.Rectangle;
|
||||
import com.badlogic.gdx.utils.Array;
|
||||
|
||||
import uno.mloluyu.util.SimpleFormatter;
|
||||
|
||||
import java.util.EnumMap;
|
||||
|
||||
public class FighterAnimationManager {
|
||||
private EnumMap<Action, Animation<TextureRegion>> animations = new EnumMap<>(Action.class);
|
||||
private EnumMap<Action, Float> frameDurations = new EnumMap<>(Action.class);
|
||||
private TextureAtlas atlas;
|
||||
private float scaleX = 1.0f;
|
||||
private float scaleY = 1.0f;
|
||||
|
||||
public FighterAnimationManager(TextureAtlas atlas) {
|
||||
this.atlas = atlas;
|
||||
for (Action action : Action.values()) {
|
||||
frameDurations.put(action, 0.1f);
|
||||
}
|
||||
}
|
||||
|
||||
public void loadAnimation(Action action, String prefix, int count, boolean loop) {
|
||||
Array<TextureRegion> frames = new Array<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
String regionName = prefix + SimpleFormatter.addLeadingZeros(i, 3);
|
||||
TextureRegion region = atlas.findRegion(regionName);
|
||||
if (region == null) {
|
||||
throw new IllegalArgumentException("未找到区域: " + regionName);
|
||||
}
|
||||
frames.add(region);
|
||||
}
|
||||
|
||||
Animation<TextureRegion> animation = new Animation<>(frameDurations.get(action), frames);
|
||||
animation.setPlayMode(loop ? Animation.PlayMode.LOOP : Animation.PlayMode.NORMAL);
|
||||
animations.put(action, animation);
|
||||
}
|
||||
|
||||
public void loadLooping(Action action, String prefix, int count) {
|
||||
loadAnimation(action, prefix, count, true);
|
||||
}
|
||||
|
||||
public void loadOneShot(Action action, String prefix, int count) {
|
||||
loadAnimation(action, prefix, count, false);
|
||||
}
|
||||
|
||||
public void setFrameDuration(Action action, float duration) {
|
||||
frameDurations.put(action, duration);
|
||||
Animation<TextureRegion> anim = animations.get(action);
|
||||
if (anim != null) anim.setFrameDuration(duration);
|
||||
}
|
||||
|
||||
public boolean isFinished(Action action, float stateTime) {
|
||||
Animation<TextureRegion> anim = animations.get(action);
|
||||
return anim != null && anim.isAnimationFinished(stateTime);
|
||||
}
|
||||
|
||||
public void render(SpriteBatch batch, Action action, float stateTime, Rectangle hitbox, boolean isFacingRight) {
|
||||
Animation<TextureRegion> anim = animations.get(action);
|
||||
if (anim == null) return;
|
||||
|
||||
TextureRegion frame = anim.getKeyFrame(stateTime, anim.getPlayMode() == Animation.PlayMode.LOOP);
|
||||
if (frame == null) return;
|
||||
|
||||
float frameWidth = frame.getRegionWidth() * scaleX;
|
||||
float frameHeight = frame.getRegionHeight() * scaleY;
|
||||
float drawX = hitbox.x + (hitbox.width - frameWidth) / 2;
|
||||
float drawY = hitbox.y;
|
||||
|
||||
boolean wasFlippedX = frame.isFlipX();
|
||||
frame.flip(!isFacingRight && !wasFlippedX, false);
|
||||
frame.flip(isFacingRight && wasFlippedX, false);
|
||||
|
||||
batch.draw(frame, drawX, drawY, frameWidth / 2, frameHeight / 2, frameWidth, frameHeight, 1f, 1f, 0f);
|
||||
frame.flip(wasFlippedX != frame.isFlipX(), false);
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
if (atlas != null) atlas.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package uno.mloluyu.characters;
|
||||
package uno.mloluyu.characters.character;
|
||||
|
||||
import com.badlogic.gdx.Gdx;
|
||||
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
|
||||
@@ -1,8 +1,10 @@
|
||||
package uno.mloluyu.characters;
|
||||
package uno.mloluyu.characters.character;
|
||||
|
||||
import com.badlogic.gdx.Gdx;
|
||||
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
|
||||
|
||||
import uno.mloluyu.characters.character.Fighter.Action;
|
||||
|
||||
public class Reimu extends Fighter {
|
||||
public Reimu() {
|
||||
super(new TextureAtlas(Gdx.files.internal("src\\main\\resources\\character\\reimu\\reimu.atlas")));
|
||||
@@ -11,9 +11,10 @@ import com.badlogic.gdx.graphics.g2d.BitmapFont;
|
||||
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
|
||||
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
|
||||
|
||||
import uno.mloluyu.characters.Alice;
|
||||
import uno.mloluyu.characters.Fighter;
|
||||
import uno.mloluyu.characters.Reimu;
|
||||
import uno.mloluyu.characters.character.Alice;
|
||||
import uno.mloluyu.characters.AdvancedFighter;
|
||||
import uno.mloluyu.characters.SimpleFighter;
|
||||
import uno.mloluyu.characters.character.Reimu;
|
||||
import uno.mloluyu.util.ClearScreen;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@@ -144,20 +145,19 @@ public class CharacterSelectScreen extends ScreenAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
// 点击确认按钮
|
||||
// 点击确认按钮
|
||||
if (isHovered(mouseX, mouseY, BUTTON_X, CONFIRM_Y, BUTTON_WIDTH, BUTTON_HEIGHT)) {
|
||||
if (selectedIndex != -1) {
|
||||
String selectedCharacter = characters.get(selectedIndex);
|
||||
Gdx.app.log("Character", "确认角色: " + selectedCharacter);
|
||||
|
||||
Fighter fighter = null;
|
||||
SimpleFighter fighter = null;
|
||||
switch (selectedCharacter) {
|
||||
case "Alice":
|
||||
fighter = new Alice();
|
||||
fighter = new AdvancedFighter("Alice");
|
||||
break;
|
||||
case "Reimu":
|
||||
fighter = new Reimu();
|
||||
fighter = new AdvancedFighter("Reimu");
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +1,29 @@
|
||||
package uno.mloluyu.desktop;
|
||||
|
||||
import com.badlogic.gdx.Gdx;
|
||||
import com.badlogic.gdx.ScreenAdapter;
|
||||
import com.badlogic.gdx.graphics.Color;
|
||||
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
|
||||
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
|
||||
import com.badlogic.gdx.math.Rectangle;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.badlogic.gdx.Gdx;
|
||||
import com.badlogic.gdx.ScreenAdapter;
|
||||
import com.badlogic.gdx.graphics.GL20;
|
||||
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
|
||||
import com.badlogic.gdx.graphics.Color;
|
||||
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
|
||||
|
||||
import uno.mloluyu.characters.Alice;
|
||||
import uno.mloluyu.characters.Fighter;
|
||||
import uno.mloluyu.characters.Reimu;
|
||||
import uno.mloluyu.characters.SimpleFighter;
|
||||
import uno.mloluyu.network.NetworkManager;
|
||||
import uno.mloluyu.util.ClearScreen;
|
||||
import uno.mloluyu.versatile.FighterController;
|
||||
|
||||
public class GameScreen extends ScreenAdapter {
|
||||
private final MainGame game;
|
||||
private final Fighter player;
|
||||
private final SimpleFighter player;
|
||||
private final FighterController controller;
|
||||
private final Map<String, SimpleFighter> otherPlayers = new HashMap<>();
|
||||
|
||||
private SpriteBatch batch;
|
||||
private ShapeRenderer shapeRenderer;
|
||||
private FighterController controller;
|
||||
private final Map<String, Fighter> otherPlayers = new HashMap<>();
|
||||
|
||||
public GameScreen(MainGame game, Fighter player) {
|
||||
this.game = game;
|
||||
public GameScreen(MainGame game, SimpleFighter player) {
|
||||
this.player = player;
|
||||
this.controller = new FighterController(player);
|
||||
}
|
||||
@@ -42,75 +39,61 @@ public class GameScreen extends ScreenAdapter {
|
||||
public void render(float delta) {
|
||||
new ClearScreen();
|
||||
|
||||
// 更新角色状态
|
||||
player.update(delta);
|
||||
controller.update(delta);
|
||||
// 发送本机玩家位置
|
||||
|
||||
if (NetworkManager.getInstance().isConnected()) {
|
||||
NetworkManager.getInstance().sendPosition(player.getX(), player.getY());
|
||||
NetworkManager.getInstance().sendPosition(player.getHitbox().x, player.getHitbox().y);
|
||||
}
|
||||
|
||||
// 渲染角色
|
||||
batch.begin();
|
||||
player.render(batch);
|
||||
batch.end();
|
||||
// 渲染其他玩家位置(联机模式)
|
||||
if (NetworkManager.getInstance().isConnected()) {
|
||||
renderOtherPlayers();
|
||||
}
|
||||
}
|
||||
shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
|
||||
|
||||
renderFighter(player, Color.BLUE);
|
||||
if (player.isAttacking()) renderAttackBox(player, Color.RED);
|
||||
|
||||
private void renderOtherPlayers() {
|
||||
Map<String, float[]> positions = NetworkManager.getInstance().getPlayerPositions();
|
||||
Map<String, String> characters = NetworkManager.getInstance().getPlayerCharacters();
|
||||
|
||||
batch.begin();
|
||||
for (Map.Entry<String, float[]> entry : positions.entrySet()) {
|
||||
String playerId = entry.getKey();
|
||||
float[] pos = entry.getValue();
|
||||
|
||||
// 跳过本机玩家(可选)
|
||||
// if (playerId.equals(NetworkManager.getInstance().getLocalPlayerId()))
|
||||
// continue;
|
||||
|
||||
// 获取角色名
|
||||
String characterName = characters.get(playerId);
|
||||
if (characterName == null || pos == null)
|
||||
continue;
|
||||
|
||||
// 获取或创建 Fighter 实例
|
||||
Fighter fighter = otherPlayers.get(playerId);
|
||||
if (fighter == null) {
|
||||
switch (characterName) {
|
||||
case "Alice":
|
||||
fighter = new Alice();
|
||||
break;
|
||||
case "Reimu":
|
||||
fighter = new Reimu();
|
||||
default:
|
||||
fighter = new Alice();
|
||||
break;
|
||||
};
|
||||
if (fighter != null) {
|
||||
otherPlayers.put(playerId, fighter);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置位置并渲染
|
||||
if (fighter != null) {
|
||||
fighter.setPosition(pos[0], pos[1]);
|
||||
fighter.render(batch);
|
||||
if (positions != null) {
|
||||
for (Map.Entry<String, float[]> entry : positions.entrySet()) {
|
||||
String id = entry.getKey();
|
||||
float[] pos = entry.getValue();
|
||||
if (pos == null) continue;
|
||||
SimpleFighter remote = otherPlayers.computeIfAbsent(id, k -> new SimpleFighter("Remote-" + k));
|
||||
remote.setPosition(pos[0], pos[1]);
|
||||
remote.update(delta);
|
||||
renderFighter(remote, Color.GREEN);
|
||||
}
|
||||
}
|
||||
batch.end();
|
||||
|
||||
shapeRenderer.end();
|
||||
}
|
||||
|
||||
private void renderFighter(SimpleFighter fighter, Color color) {
|
||||
shapeRenderer.setColor(color);
|
||||
Rectangle r = fighter.getHitbox();
|
||||
shapeRenderer.rect(r.x, r.y, r.width, r.height);
|
||||
}
|
||||
|
||||
private void renderAttackBox(SimpleFighter fighter, Color color) {
|
||||
shapeRenderer.setColor(color);
|
||||
Rectangle a = fighter.getAttackbox();
|
||||
shapeRenderer.rect(a.x, a.y, a.width, a.height);
|
||||
}
|
||||
|
||||
// private void checkPlayerAttacks() {
|
||||
// if (!player.isAttacking()) return;
|
||||
|
||||
// for (SimpleFighter target : otherPlayers.values()) {
|
||||
// if (target.isAlive() && player.getAttackbox().overlaps(target.getHitbox())) {
|
||||
// target.takeHit(player.getAttackPower()); // 使用访问器方法
|
||||
// System.out.println("命中远程玩家:" + target.getName());
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
batch.dispose();
|
||||
player.dispose();
|
||||
shapeRenderer.dispose();
|
||||
// 断开网络连接
|
||||
NetworkManager.getInstance().disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ public class MainGame extends Game {
|
||||
mainMenuScreen = new MainMenuScreen(this);
|
||||
setScreen(new MainMenuScreen(this));
|
||||
setScreen(startScreen);
|
||||
|
||||
}
|
||||
|
||||
public void showGameScreen() {
|
||||
|
||||
@@ -8,7 +8,7 @@ import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
|
||||
import com.badlogic.gdx.graphics.g2d.BitmapFont;
|
||||
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
|
||||
|
||||
import uno.mloluyu.characters.Alice;
|
||||
import uno.mloluyu.characters.character.Alice;
|
||||
import uno.mloluyu.versatile.FighterController;
|
||||
|
||||
import static uno.mloluyu.util.Font.loadChineseFont;
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
package uno.mloluyu.desktop;
|
||||
import com.badlogic.gdx.Gdx;
|
||||
import com.badlogic.gdx.Screen;
|
||||
import com.badlogic.gdx.graphics.GL20;
|
||||
import com.badlogic.gdx.graphics.Color;
|
||||
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
|
||||
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
|
||||
|
||||
/**
|
||||
* 屏幕过渡效果类
|
||||
* 用于在不同屏幕之间实现平滑的过渡动画
|
||||
*/
|
||||
public class TransitionScreen implements Screen {
|
||||
private ShapeRenderer shapeRenderer;
|
||||
private SpriteBatch batch;
|
||||
private float transitionTime = 0;
|
||||
private float totalTransitionTime = 0.5f; // 过渡时间为0.5秒
|
||||
private Runnable targetScreenAction;
|
||||
private TransitionType transitionType = TransitionType.FADE_OUT_FADE_IN;
|
||||
|
||||
/**
|
||||
* 过渡类型枚举
|
||||
*/
|
||||
public enum TransitionType {
|
||||
FADE_OUT_FADE_IN
|
||||
}
|
||||
|
||||
public TransitionScreen() {
|
||||
this.shapeRenderer = new ShapeRenderer();
|
||||
this.batch = new SpriteBatch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置目标屏幕的显示动作
|
||||
*
|
||||
* @param targetScreenAction 目标屏幕的显示动作
|
||||
*/
|
||||
public void setTargetScreen(Runnable targetScreenAction) {
|
||||
this.targetScreenAction = targetScreenAction;
|
||||
this.transitionTime = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show() {
|
||||
// 屏幕显示时的初始化操作
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(float delta) {
|
||||
// 清屏
|
||||
Gdx.gl.glClearColor(0, 0, 0, 1);
|
||||
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
|
||||
|
||||
transitionTime += delta;
|
||||
float progress = Math.min(transitionTime / totalTransitionTime, 1);
|
||||
|
||||
// 绘制过渡效果
|
||||
shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
|
||||
shapeRenderer.setColor(0, 0, 0, getAlpha(progress));
|
||||
shapeRenderer.rect(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
|
||||
shapeRenderer.end();
|
||||
|
||||
// 当过渡动画完成时,执行目标屏幕的显示动作
|
||||
if (progress >= 1 && targetScreenAction != null) {
|
||||
targetScreenAction.run();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据过渡进度计算透明度
|
||||
*
|
||||
* @param progress 过渡进度(0-1)
|
||||
* @return 透明度值(0-1)
|
||||
*/
|
||||
private float getAlpha(float progress) {
|
||||
switch (transitionType) {
|
||||
case FADE_OUT_FADE_IN:
|
||||
// 前半段淡出(透明度从0到1),后半段淡入(透明度从1到0)
|
||||
if (progress < 0.5) {
|
||||
return progress * 2;
|
||||
} else {
|
||||
return (1 - progress) * 2;
|
||||
}
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resize(int width, int height) {
|
||||
// 屏幕尺寸变化时的处理
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pause() {
|
||||
// 游戏暂停时的处理
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resume() {
|
||||
// 游戏恢复时的处理
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hide() {
|
||||
// 屏幕隐藏时的处理
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
// 释放资源
|
||||
if (shapeRenderer != null) {
|
||||
shapeRenderer.dispose();
|
||||
shapeRenderer = null;
|
||||
}
|
||||
|
||||
if (batch != null) {
|
||||
batch.dispose();
|
||||
batch = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ public class NetworkManager {
|
||||
private ConnectServer server;
|
||||
private ConnectClient client;
|
||||
private boolean isHost = false;
|
||||
|
||||
private String localPlayerId;
|
||||
private String localCharacter;
|
||||
private final Map<String, float[]> playerPositions = new HashMap<>();
|
||||
@@ -31,31 +30,31 @@ public class NetworkManager {
|
||||
return localPlayerId;
|
||||
}
|
||||
|
||||
public void createRoom() {
|
||||
public void createRoom() {//创建房间
|
||||
isHost = true;
|
||||
server = new ConnectServer(11455);
|
||||
new Thread(server).start();
|
||||
Gdx.app.log("Network", "房主模式:服务器已启动");
|
||||
}
|
||||
|
||||
public void joinRoom(String ip) {
|
||||
public void joinRoom(String ip) {//加入房间
|
||||
isHost = false;
|
||||
client = new ConnectClient(ip, 11455);
|
||||
Gdx.app.log("Network", "客户端模式:连接到房主 " + ip);
|
||||
}
|
||||
|
||||
public void sendPosition(float x, float y) {
|
||||
public void sendPosition(float x, float y) {//发送位置消息
|
||||
String msg = "POS:" + localPlayerId + "," + x + "," + y;
|
||||
Gdx.app.log("Network", "发送位置消息: " + msg);
|
||||
if (isHost && server != null) {
|
||||
server.broadcastToOthers(null, msg);
|
||||
receiveMessage(msg); // 房主自己也处理
|
||||
receiveMessage(msg);
|
||||
} else if (client != null) {
|
||||
client.sendMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendCharacterSelection(String character) {
|
||||
public void sendCharacterSelection(String character) {//发送角色选择消息
|
||||
this.localCharacter = character;
|
||||
String msg = "SELECT:" + localPlayerId + "," + character;
|
||||
Gdx.app.log("Network", "发送角色选择消息: " + msg);
|
||||
@@ -67,7 +66,7 @@ public class NetworkManager {
|
||||
}
|
||||
}
|
||||
|
||||
public void receiveMessage(String message) {
|
||||
public void receiveMessage(String message) {//解析消息
|
||||
Gdx.app.log("Network", "收到消息: " + message);
|
||||
|
||||
if (message.startsWith("POS:")) {
|
||||
|
||||
@@ -1,131 +1,66 @@
|
||||
package uno.mloluyu.versatile;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.badlogic.gdx.Gdx;
|
||||
import com.badlogic.gdx.Input;
|
||||
import com.badlogic.gdx.InputAdapter;
|
||||
import com.badlogic.gdx.utils.Array;
|
||||
import uno.mloluyu.characters.Fighter;
|
||||
import uno.mloluyu.characters.character.Action;
|
||||
import uno.mloluyu.characters.character.Fighter;
|
||||
import uno.mloluyu.characters.SimpleFighter;
|
||||
|
||||
/**
|
||||
* 角色控制器,处理玩家输入并映射到角色动作
|
||||
*/
|
||||
public class FighterController extends InputAdapter {
|
||||
private final Fighter fighter;
|
||||
private final SimpleFighter fighter;
|
||||
private final Array<Integer> pressedKeys = new Array<>();
|
||||
|
||||
// 输入缓冲时间(防止快速按键导致的重复触发)
|
||||
private static final float INPUT_DELAY = 0.2f;
|
||||
private float attackCooldown = 0;
|
||||
private float jumpCooldown = 0;
|
||||
private final Map<Integer, Float> keyPressDuration = new HashMap<>();
|
||||
|
||||
public FighterController(Fighter fighter) {
|
||||
public FighterController(SimpleFighter fighter) {
|
||||
this.fighter = fighter;
|
||||
}
|
||||
|
||||
public FighterController() {
|
||||
this.fighter = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新控制器状态和冷却时间
|
||||
* @param deltaTime 帧间隔时间
|
||||
*/
|
||||
public void update(float deltaTime) {
|
||||
// 更新冷却时间
|
||||
if (attackCooldown > 0) attackCooldown -= deltaTime;
|
||||
if (jumpCooldown > 0) jumpCooldown -= deltaTime;
|
||||
|
||||
// 处理移动输入
|
||||
handleMovement();
|
||||
|
||||
if (fighter == null)
|
||||
return;
|
||||
|
||||
for (int keycode : pressedKeys) {
|
||||
fighter.handleInput(keycode, true); // 持续按下的键
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理移动输入
|
||||
*/
|
||||
private void handleMovement() {
|
||||
float moveX = 0;
|
||||
|
||||
// 左右移动控制(支持方向键和WSAD)
|
||||
if (isKeyPressed(Input.Keys.RIGHT) || isKeyPressed(Input.Keys.D)) {
|
||||
moveX += 1;
|
||||
}
|
||||
if (isKeyPressed(Input.Keys.LEFT) || isKeyPressed(Input.Keys.A)) {
|
||||
moveX -= 1;
|
||||
}
|
||||
|
||||
// 调用角色移动方法
|
||||
fighter.move(moveX, Gdx.graphics.getDeltaTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理按键按下事件
|
||||
*/
|
||||
@Override
|
||||
public boolean keyDown(int keycode) {
|
||||
if (fighter == null)
|
||||
return false;
|
||||
|
||||
if (!pressedKeys.contains(keycode, false)) {
|
||||
pressedKeys.add(keycode);
|
||||
}
|
||||
|
||||
// 普通攻击(Z键或J键)
|
||||
if ((keycode == Input.Keys.Z || keycode == Input.Keys.J) && attackCooldown <= 0) {
|
||||
fighter.attack(1);
|
||||
attackCooldown = INPUT_DELAY;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 第二攻击(X键或K键)
|
||||
if ((keycode == Input.Keys.X || keycode == Input.Keys.K) && attackCooldown <= 0) {
|
||||
fighter.attack(2);
|
||||
attackCooldown = INPUT_DELAY;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// 跳跃(空格、上方向键或W键)
|
||||
if ((keycode == Input.Keys.SPACE || keycode == Input.Keys.UP || keycode == Input.Keys.W) && jumpCooldown <= 0) {
|
||||
// 这里假设你已经实现了跳跃方法
|
||||
// fighter.jump();
|
||||
jumpCooldown = INPUT_DELAY;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 防御(左Shift或右Shift)
|
||||
if (keycode == Input.Keys.SHIFT_LEFT || keycode == Input.Keys.SHIFT_RIGHT) {
|
||||
fighter.changeAction(Fighter.Action.DEFEND);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
fighter.handleInput(keycode, true); // 按下事件
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理按键释放事件
|
||||
*/
|
||||
@Override
|
||||
public boolean keyUp(int keycode) {
|
||||
pressedKeys.removeValue(keycode, false);
|
||||
|
||||
// 释放防御键时恢复到 idle 状态
|
||||
if ((keycode == Input.Keys.SHIFT_LEFT || keycode == Input.Keys.SHIFT_RIGHT) &&
|
||||
fighter.getCurrentAction() == Fighter.Action.DEFEND) {
|
||||
fighter.changeAction(Fighter.Action.IDLE);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public boolean keyUp(int keycode) {
|
||||
if (fighter == null)
|
||||
return false;
|
||||
|
||||
/**
|
||||
* 检查按键是否处于按下状态
|
||||
*/
|
||||
private boolean isKeyPressed(int keycode) {
|
||||
return pressedKeys.contains(keycode, false);
|
||||
}
|
||||
float duration = keyPressDuration.getOrDefault(keycode, 0f);
|
||||
pressedKeys.removeValue(keycode, false);
|
||||
keyPressDuration.remove(keycode);
|
||||
|
||||
/**
|
||||
* 获取当前控制的角色
|
||||
*/
|
||||
public Fighter getFighter() {
|
||||
// 传给角色:按键松开 + 按下时长
|
||||
fighter.handleRelease(keycode, duration);
|
||||
return true;
|
||||
}//松开事件
|
||||
|
||||
public SimpleFighter getFighter() {
|
||||
return fighter;
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 19 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 19 KiB |
BIN
target/classes/uno/mloluyu/characters/Action.class
Normal file
BIN
target/classes/uno/mloluyu/characters/Action.class
Normal file
Binary file not shown.
BIN
target/classes/uno/mloluyu/characters/AdvancedFighter.class
Normal file
BIN
target/classes/uno/mloluyu/characters/AdvancedFighter.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
target/classes/uno/mloluyu/characters/SimpleFighter.class
Normal file
BIN
target/classes/uno/mloluyu/characters/SimpleFighter.class
Normal file
Binary file not shown.
BIN
target/classes/uno/mloluyu/characters/character/Action.class
Normal file
BIN
target/classes/uno/mloluyu/characters/character/Action.class
Normal file
Binary file not shown.
BIN
target/classes/uno/mloluyu/characters/character/Alice.class
Normal file
BIN
target/classes/uno/mloluyu/characters/character/Alice.class
Normal file
Binary file not shown.
Binary file not shown.
BIN
target/classes/uno/mloluyu/characters/character/Fighter.class
Normal file
BIN
target/classes/uno/mloluyu/characters/character/Fighter.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
target/classes/uno/mloluyu/characters/character/Reimu.class
Normal file
BIN
target/classes/uno/mloluyu/characters/character/Reimu.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user