本节课目标
-
按空格键向鼠标方向发射子弹
-
子弹碰到敌人后,敌人扣血
-
子弹碰到墙壁后消失
-
敌人被子弹打死才消失(不再碰撞扣血)
第一步:创建子弹类
右键项目 → 添加 → 类 ,文件名 Bullet.cs:
csharp
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace MY_FIRST_GAME
{
public class Bullet
{
public Vector2 Position;
public Vector2 Velocity;
public float Speed = 400f;
public int Damage = 20;
public bool IsAlive = true;
public float Lifetime = 2f; // 最多存在2秒
private Texture2D texture;
private float timer;
public Bullet(Vector2 startPosition, Vector2 direction, GraphicsDevice graphicsDevice)
{
Position = startPosition;
Velocity = direction * Speed;
// 创建8x8的黄色子弹纹理
texture = new Texture2D(graphicsDevice, 8, 8);
Color[] data = new Color[8 * 8];
for (int y = 0; y < 8; y++)
for (int x = 0; x < 8; x++)
{
float dx = x - 3.5f;
float dy = y - 3.5f;
if (dx * dx + dy * dy < 16)
data[y * 8 + x] = Color.Yellow;
else
data[y * 8 + x] = Color.Transparent;
}
texture.SetData(data);
}
public void Update(float deltaTime, TileMap tileMap)
{
Position += Velocity * deltaTime;
timer += deltaTime;
// 超时或撞墙就消失
if (timer >= Lifetime || tileMap.IsWall(Position))
IsAlive = false;
}
public void Draw(SpriteBatch spriteBatch)
{
spriteBatch.Draw(texture, Position, null, Color.White,
0f, new Vector2(4, 4), 1f, SpriteEffects.None, 0f);
}
public Rectangle GetBounds()
{
return new Rectangle((int)Position.X - 4, (int)Position.Y - 4, 8, 8);
}
}
}
第二步:给 Player 类添加攻击方法
在 Player.cs 里添加:
cs
// 添加字段
private float attackCooldown;
private float attackTimer;
// 在构造函数里初始化
attackCooldown = 0.3f; // 0.3秒攻击间隔
attackTimer = 0f;
// 添加方法:返回是否应该发射子弹
public bool TryAttack(float deltaTime)
{
attackTimer += deltaTime;
if (attackTimer >= attackCooldown)
{
attackTimer = 0f;
return true;
}
return false;
}
// 获取瞄准方向(指向鼠标)
public Vector2 GetAimDirection(Vector2 mouseWorldPosition)
{
Vector2 direction = mouseWorldPosition - Position;
if (direction.Length() > 0)
direction.Normalize();
else
direction = new Vector2(1, 0); // 默认向右
return direction;
}
第三步:在 Game1.cs 中集成子弹
把 Game1.cs 完整替换为:
cs
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Audio;
using System;
using System.Collections.Generic;
using System.IO;
using FontStashSharp;
namespace MY_FIRST_GAME
{
public class Game1 : Game
{
private GraphicsDeviceManager _graphics;
private SpriteBatch _spriteBatch;
private SceneManager sceneManager = default!;
private Camera camera = default!;
private Player player = default!;
private Texture2D playerSpriteSheet;
private TileMap tileMap = default!;
private Texture2D coinTexture;
private List<Vector2> coins = default!;
private Random rng = default!;
private int score;
private List<Enemy> enemies = default!;
private SpriteFontBase font = default!;
private SpriteFontBase titleFont = default!;
private SoundEffect coinSound = default!;
private SoundEffect hitSound = default!;
private SoundEffect shootSound = default!;
private ParticleSystem particleSystem = default!;
private List<Bullet> bullets = default!;
private float titleTimer;
private SaveData saveData = default!;
private int coinsCollectedThisGame;
private int enemiesDefeatedThisGame;
private bool isNewHighScore;
private MouseState currentMouse;
private MouseState previousMouse;
public Game1()
{
_graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
IsMouseVisible = true;
}
protected override void Initialize()
{
_graphics.PreferredBackBufferWidth = 800;
_graphics.PreferredBackBufferHeight = 600;
_graphics.ApplyChanges();
sceneManager = new SceneManager();
rng = new Random();
saveData = SaveManager.Load();
camera = new Camera(800, 600, 1600, 1200);
InitializeGame();
base.Initialize();
}
private void InitializeGame()
{
tileMap = new TileMap(GraphicsDevice);
coins = new List<Vector2>();
enemies = new List<Enemy>();
bullets = new List<Bullet>();
score = 0;
coinsCollectedThisGame = 0;
enemiesDefeatedThisGame = 0;
isNewHighScore = false;
titleTimer = 0f;
SpawnCoins(10);
SpawnEnemies(8);
particleSystem = new ParticleSystem(GraphicsDevice);
}
protected override void LoadContent()
{
_spriteBatch = new SpriteBatch(GraphicsDevice);
using var stream = File.OpenRead("Content/player_spritesheet.png");
playerSpriteSheet = Texture2D.FromStream(GraphicsDevice, stream);
player = new Player(playerSpriteSheet, tileMap.GetRandomEmptyPosition(rng), tileMap);
coinTexture = new Texture2D(GraphicsDevice, 24, 24);
Color[] coinData = new Color[24 * 24];
for (int i = 0; i < coinData.Length; i++) coinData[i] = Color.Gold;
coinTexture.SetData(coinData);
var fontSystem = new FontSystem();
fontSystem.AddFont(File.ReadAllBytes("Content/consola.ttf"));
font = fontSystem.GetFont(18);
titleFont = fontSystem.GetFont(48);
try
{
using var coinStream = File.OpenRead("Content/coin.wav");
coinSound = SoundEffect.FromStream(coinStream);
using var hitStream = File.OpenRead("Content/hit.wav");
hitSound = SoundEffect.FromStream(hitStream);
using var shootStream = File.OpenRead("Content/shoot.wav");
shootSound = SoundEffect.FromStream(shootStream);
}
catch { }
}
private void SpawnCoins(int count)
{
for (int i = 0; i < count; i++)
coins.Add(tileMap.GetRandomEmptyPosition(rng));
}
private void SpawnEnemies(int count)
{
for (int i = 0; i < count; i++)
{
Vector2 pos = tileMap.GetRandomEmptyPosition(rng);
EnemyType type = (EnemyType)(rng.Next(3));
enemies.Add(new Enemy(type, pos, GraphicsDevice));
}
}
protected override void Update(GameTime gameTime)
{
float deltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
KeyboardState keyboard = Keyboard.GetState();
currentMouse = Mouse.GetState();
titleTimer += deltaTime;
sceneManager.Update();
switch (sceneManager.CurrentScene)
{
case SceneType.Title:
if (keyboard.IsKeyDown(Keys.Space))
{
player = new Player(playerSpriteSheet, tileMap.GetRandomEmptyPosition(rng), tileMap);
InitializeGame();
sceneManager.GoToGame();
}
break;
case SceneType.Game:
player.Update(deltaTime);
camera.Follow(player.Position);
camera.UpdateMatrix();
// ★ 发射子弹
if (currentMouse.LeftButton == ButtonState.Pressed &&
previousMouse.LeftButton == ButtonState.Released)
{
if (player.TryAttack(deltaTime))
{
Vector2 mouseWorld = camera.ScreenToWorld(
new Vector2(currentMouse.X, currentMouse.Y));
Vector2 aimDir = player.GetAimDirection(mouseWorld);
bullets.Add(new Bullet(player.Position, aimDir, GraphicsDevice));
try { shootSound?.Play(); } catch { }
}
}
// 更新敌人
foreach (Enemy enemy in enemies)
enemy.Update(deltaTime, player.Position, tileMap);
// 更新子弹
foreach (Bullet bullet in bullets)
bullet.Update(deltaTime, tileMap);
// ★ 检测子弹和敌人碰撞
CheckBulletEnemyCollision();
// 检测玩家和敌人碰撞(不再扣血,只推开?或者保留扣血)
CheckEnemyCollision();
CheckCoinCollision();
// 清理死亡敌人和子弹
enemies.RemoveAll(e => !e.IsAlive);
bullets.RemoveAll(b => !b.IsAlive);
if (coins.Count == 0) SpawnCoins(10);
if (enemies.Count == 0) SpawnEnemies(8);
particleSystem.Update(deltaTime);
if (player.Hp <= 0)
{
if (score > saveData.HighScore)
{
saveData.HighScore = score;
isNewHighScore = true;
}
saveData.TotalCoinsCollected += coinsCollectedThisGame;
saveData.TotalEnemiesDefeated += enemiesDefeatedThisGame;
SaveManager.Save(saveData);
sceneManager.GoToGameOver();
}
break;
case SceneType.GameOver:
break;
}
previousMouse = currentMouse;
if (keyboard.IsKeyDown(Keys.M))
SoundEffect.MasterVolume = SoundEffect.MasterVolume > 0 ? 0f : 1f;
if (keyboard.IsKeyDown(Keys.Escape))
Exit();
base.Update(gameTime);
}
private void CheckBulletEnemyCollision()
{
for (int i = bullets.Count - 1; i >= 0; i--)
{
for (int j = enemies.Count - 1; j >= 0; j--)
{
if (bullets[i].GetBounds().Intersects(enemies[j].GetBounds()))
{
enemies[j].Hp -= bullets[i].Damage;
bullets[i].IsAlive = false;
if (enemies[j].Hp <= 0)
{
enemies[j].IsAlive = false;
score += enemies[j].ScoreValue;
enemiesDefeatedThisGame++;
particleSystem.EmitHitParticles(enemies[j].Position);
}
break;
}
}
}
}
private void CheckEnemyCollision()
{
Rectangle playerRect = player.GetBounds();
for (int i = enemies.Count - 1; i >= 0; i--)
{
if (enemies[i].GetBounds().Intersects(playerRect))
{
player.Hp -= enemies[i].Attack / 2; // 碰撞伤害减半
// 把敌人推开
Vector2 pushDir = enemies[i].Position - player.Position;
if (pushDir.Length() > 0)
{
pushDir.Normalize();
enemies[i].Position += pushDir * 30;
}
}
}
}
private void CheckCoinCollision()
{
Rectangle playerRect = player.GetBounds();
for (int i = coins.Count - 1; i >= 0; i--)
{
Rectangle coinRect = new Rectangle((int)coins[i].X - 12, (int)coins[i].Y - 12, 24, 24);
if (playerRect.Intersects(coinRect))
{
Vector2 coinPos = coins[i];
coins.RemoveAt(i);
score += 10;
coinsCollectedThisGame++;
particleSystem.EmitCoinParticles(coinPos);
try { coinSound?.Play(); } catch { }
}
}
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
switch (sceneManager.CurrentScene)
{
case SceneType.Title:
_spriteBatch.Begin();
DrawTitle();
_spriteBatch.End();
break;
case SceneType.Game:
_spriteBatch.Begin(transformMatrix: camera.Transform);
DrawGameWorld();
_spriteBatch.End();
// UI 层
_spriteBatch.Begin();
DrawGameUI();
// ★ 画准星线
DrawCrosshair();
_spriteBatch.End();
break;
case SceneType.GameOver:
_spriteBatch.Begin();
DrawGameOver();
_spriteBatch.End();
break;
}
base.Draw(gameTime);
}
private void DrawTitle()
{
string title = "史莱姆猎人";
Vector2 titleSize = titleFont.MeasureString(title);
float alpha = (float)(Math.Sin(titleTimer * 3) + 1) / 2 * 0.5f + 0.5f;
_spriteBatch.DrawString(titleFont, title,
new Vector2(400 - titleSize.X / 2, 120), Color.Gold * alpha);
_spriteBatch.DrawString(font, "收集金币,消灭史莱姆!",
new Vector2(400 - font.MeasureString("收集金币,消灭史莱姆!").X / 2, 200), Color.LightGray);
_spriteBatch.DrawString(font, $"🏆 最高分:{saveData.HighScore}", new Vector2(300, 260), Color.Gold);
_spriteBatch.DrawString(font, $"💰 累计金币:{saveData.TotalCoinsCollected}", new Vector2(300, 285), Color.Yellow);
_spriteBatch.DrawString(font, $"💀 累计击杀:{saveData.TotalEnemiesDefeated}", new Vector2(300, 310), Color.Red);
string prompt = "按 空格键 开始游戏 | 鼠标左键射击";
_spriteBatch.DrawString(font, prompt,
new Vector2(400 - font.MeasureString(prompt).X / 2, 400), Color.White);
}
private void DrawGameWorld()
{
tileMap.Draw(_spriteBatch);
foreach (Vector2 coinPos in coins)
_spriteBatch.Draw(coinTexture, coinPos, null, Color.White,
0f, new Vector2(12, 12), 1f, SpriteEffects.None, 0f);
foreach (Enemy enemy in enemies)
enemy.Draw(_spriteBatch);
// ★ 画子弹
foreach (Bullet bullet in bullets)
bullet.Draw(_spriteBatch);
particleSystem.Draw(_spriteBatch);
player.Draw(_spriteBatch);
}
private void DrawGameUI()
{
_spriteBatch.DrawString(font, $"分数:{score}", new Vector2(10, 10), Color.White);
_spriteBatch.DrawString(font, $"最高分:{saveData.HighScore}", new Vector2(10, 35), Color.Gold);
_spriteBatch.DrawString(font, $"HP:{player.Hp}/{player.MaxHp}", new Vector2(10, 60), Color.LimeGreen);
_spriteBatch.DrawString(font, $"敌人:{enemies.Count} | 子弹:{bullets.Count}",
new Vector2(10, 85), Color.Yellow);
_spriteBatch.DrawString(font, "WASD移动 | 鼠标瞄准左键射击 | M静音",
new Vector2(10, 570), Color.LightGray);
}
// ★ 画玩家到鼠标的瞄准线
private void DrawCrosshair()
{
Vector2 playerScreen = camera.WorldToScreen(player.Position);
Vector2 mouseScreen = new Vector2(currentMouse.X, currentMouse.Y);
// 画一条短线表示瞄准方向
Vector2 dir = mouseScreen - playerScreen;
if (dir.Length() > 0)
dir.Normalize();
Vector2 lineEnd = playerScreen + dir * 30;
// 用简单的点来画线
for (int i = 0; i < 30; i += 2)
{
Vector2 dot = playerScreen + dir * i;
Texture2D dotTex = new Texture2D(GraphicsDevice, 2, 2);
dotTex.SetData(new[] { Color.White * 0.5f });
_spriteBatch.Draw(dotTex, dot, Color.White);
}
}
private void DrawGameOver()
{
string title = "游戏结束";
_spriteBatch.DrawString(titleFont, title,
new Vector2(400 - titleFont.MeasureString(title).X / 2, 100), Color.Red);
_spriteBatch.DrawString(font, $"本局分数:{score}",
new Vector2(400 - font.MeasureString($"本局分数:{score}").X / 2, 190), Color.White);
_spriteBatch.DrawString(font, $"🏆 最高分:{saveData.HighScore}",
new Vector2(400 - font.MeasureString($"🏆 最高分:{saveData.HighScore}").X / 2, 220), Color.Gold);
if (isNewHighScore)
_spriteBatch.DrawString(font, "🎉 新纪录!",
new Vector2(400 - font.MeasureString("🎉 新纪录!").X / 2, 255), Color.Orange);
string prompt = "按 空格键 返回标题画面";
_spriteBatch.DrawString(font, prompt,
new Vector2(400 - font.MeasureString(prompt).X / 2, 400), Color.White);
}
}
}
好了,本节课学习到此结束,我是魔法阵维护师,关注我,下期更精彩!