一款简约游戏程序

只需要安装 Pygame:

pip install pygame

把下面代码保存为 dnf.html,双击用 Chrome / Edge 打开即可。

<!DOCTYPE html>

<html lang="zh-CN">

<head>

<meta charset="UTF-8">

<title>DNF 简约版</title>

<style>

html,body{margin:0;height:100%;background:#0d0d12;display:flex;

align-items:center;justify-content:center;font-family:system-ui,sans-serif}

canvas{border:2px solid #333;border-radius:6px;box-shadow:0 0 30px #000}

</style>

</head>

<body>

<canvas id="game" width="960" height="540"></canvas>

<script>

(() => {

const cvs = document.getElementById('game');

const ctx = cvs.getContext('2d');

const W = cvs.width, H = cvs.height;

const GROUND = 440;

const GRAVITY = 0.8;

// ================= 输入 =================

const keys = {};

let jumpRequested = false;

const blockKeys = ' ', 'arrowup', 'arrowdown', 'arrowleft', 'arrowright';

addEventListener('keydown', e => {

const k = e.key.toLowerCase();

if (blockKeys.includes(k)) e.preventDefault();

if (keysk) return; // 忽略按住重复触发

keysk = true;

if (k === 'w' || k === 'arrowup' || k === ' ') jumpRequested = true;

if (k === 'j' || k === 'z') player && player.attack();

if (k === 'r' && state !== 'playing') reset();

});

addEventListener('keyup', e => { keyse.key.toLowerCase() = false; });

// ================= 碰撞 =================

const hit = (a, b) =>

a.x < b.x + b.w && a.x + a.w > b.x &&

a.y < b.y + b.h && a.y + a.h > b.y;

// ================= 角色基类 =================

class Fighter {

constructor(x, y, w, h, color, maxHp) {

this.x = x; this.y = y; this.w = w; this.h = h;

this.color = color;

this.maxHp = maxHp; this.hp = maxHp;

this.vy = 0; this.onGround = false;

this.facing = 1;

this.attackTimer = 0;

this.hitTimer = 0;

}

get box() { return { x: this.x, y: this.y, w: this.w, h: this.h }; }

gravity() {

this.vy += GRAVITY;

this.y += this.vy;

if (this.y + this.h >= GROUND) {

this.y = GROUND - this.h;

this.vy = 0;

this.onGround = true;

} else this.onGround = false;

}

attackBox() {

if (this.attackTimer <= 0) return null;

const aw = 50, ah = 50;

return {

x: this.facing === 1 ? this.x + this.w : this.x - aw,

y: this.y + this.h / 2 - ah / 2,

w: aw, h: ah

};

}

draw() {

ctx.fillStyle = this.hitTimer > 0 ? '#ffffff' : this.color;

ctx.fillRect(this.x, this.y, this.w, this.h);

// 血条

const r = Math.max(0, this.hp / this.maxHp);

ctx.fillStyle = '#333';

ctx.fillRect(this.x, this.y - 12, this.w, 6);

ctx.fillStyle = r > 0.4 ? '#4caf50' : '#e0a030';

ctx.fillRect(this.x, this.y - 12, this.w * r, 6);

// 攻击框提示

const ab = this.attackBox();

if (ab) {

ctx.strokeStyle = '#ffd54f';

ctx.lineWidth = 2;

ctx.strokeRect(ab.x, ab.y, ab.w, ab.h);

}

}

}

// ================= 玩家 =================

class Player extends Fighter {

constructor() {

super(120, GROUND - 70, 40, 70, '#4a8fe0', 100);

}

update() {

if (this.hitTimer > 0) {

this.hitTimer--;

this.gravity();

return;

}

if (this.attackTimer > 0) this.attackTimer--;

let dx = 0;

if (keys'a' || keys'arrowleft') { dx = -5; this.facing = -1; }

if (keys'd' || keys'arrowright') { dx = 5; this.facing = 1; }

this.x = Math.max(0, Math.min(W - this.w, this.x + dx));

if (jumpRequested && this.onGround) {

this.vy = -14;

this.onGround = false;

}

jumpRequested = false;

this.gravity();

}

attack() {

if (this.attackTimer <= 0 && this.hitTimer <= 0) this.attackTimer = 12;

}

}

// ================= 敌人 =================

class Enemy extends Fighter {

constructor(x, level = 1) {

super(x, GROUND - 60, 36, 60, '#c84a4a', 40 + level * 20);

this.speed = 1.6 + level * 0.4;

this.cd = 0;

}

update(player) {

if (this.attackTimer > 0) this.attackTimer--;

if (this.cd > 0) this.cd--;

if (this.hitTimer > 0) {

this.hitTimer--;

this.gravity();

return;

}

const dist = (player.x + player.w / 2) - (this.x + this.w / 2);

this.facing = dist > 0 ? 1 : -1;

if (Math.abs(dist) > 55) this.x += this.facing * this.speed;

if (Math.abs(dist) < 75 && this.cd <= 0) {

this.attackTimer = 15;

this.cd = 70;

}

this.gravity();

}

}

// ================= 状态 =================

let player, enemies, effects, state;

function reset() {

player = new Player();

enemies = [

new Enemy(320, 1),

new Enemy(560, 1),

new Enemy(780, 2)

];

effects = \[\];

state = 'playing';

jumpRequested = false;

}

// ================= 更新 =================

function update() {

if (state !== 'playing') return;

player.update();

// 玩家攻击判定

const pb = player.attackBox();

if (pb) {

for (let i = enemies.length - 1; i >= 0; i--) {

const e = enemiesi;

if (e.hitTimer > 0) continue;

if (hit(pb, e.box)) {

e.hp -= 12;

e.hitTimer = 8;

e.attackTimer = 0;

effects.push({ x: e.x + e.w / 2, y: e.y + e.h / 2, t: 10 });

if (e.hp <= 0) enemies.splice(i, 1);

}

}

}

// 敌人更新 + 攻击判定

for (const e of enemies) {

e.update(player);

const eb = e.attackBox();

if (eb && player.hitTimer <= 0 && !pb && hit(eb, player.box)) {

player.hp -= 8;

player.hitTimer = 15;

player.attackTimer = 0;

player.x += -e.facing * 25;

player.x = Math.max(0, Math.min(W - player.w, player.x));

}

}

// 打击特效衰减

for (let i = effects.length - 1; i >= 0; i--) {

if (--effectsi.t <= 0) effects.splice(i, 1);

}

// 胜负

if (player.hp <= 0) state = 'lose';

else if (enemies.length === 0) state = 'win';

}

// ================= 渲染 =================

function render() {

// 背景

ctx.fillStyle = '#1a1a24';

ctx.fillRect(0, 0, W, H);

ctx.strokeStyle = '#2c2c3c';

ctx.lineWidth = 1;

for (let i = 0; i < 8; i++) {

ctx.strokeRect(i * 140 + 20, GROUND - 170, 90, 170);

}

// 地面

ctx.fillStyle = '#3a3330';

ctx.fillRect(0, GROUND, W, H - GROUND);

ctx.strokeStyle = '#6a5a4a';

ctx.beginPath();

ctx.moveTo(0, GROUND);

ctx.lineTo(W, GROUND);

ctx.stroke();

// 实体

for (const e of enemies) e.draw();

player.draw();

// 打击特效

for (const fx of effects) {

const k = fx.t / 10;

ctx.strokeStyle = `rgba(255,213,79,${k})`;

ctx.lineWidth = 2;

ctx.beginPath();

ctx.arc(fx.x, fx.y, 8 + (1 - k) * 18, 0, Math.PI * 2);

ctx.stroke();

}

// HUD

ctx.fillStyle = '#fff';

ctx.font = '20px monospace';

ctx.fillText(`HP ${Math.max(0, player.hp)}/100`, 20, 30);

ctx.fillText(`敌人 ${enemies.length}`, 20, 56);

ctx.fillStyle = '#888';

ctx.font = '16px monospace';

ctx.fillText('A/D 移动 Space/W 跳跃 J/Z 攻击', 20, H - 20);

// 结算

if (state !== 'playing') {

ctx.fillStyle = 'rgba(0,0,0,0.72)';

ctx.fillRect(0, 0, W, H);

ctx.textAlign = 'center';

ctx.font = 'bold 48px monospace';

ctx.fillStyle = state === 'win' ? '#ffd54f' : '#e84a4a';

ctx.fillText(state === 'win' ? 'DUNGEON CLEAR' : 'YOU DIED', W / 2, H / 2 - 20);

ctx.fillStyle = '#ccc';

ctx.font = '20px monospace';

ctx.fillText('按 R 重新开始', W / 2, H / 2 + 40);

ctx.textAlign = 'start';

}

}

// ================= 主循环 =================

function loop() {

update();

render();

requestAnimationFrame(loop);

}

reset();

loop();

})();

</script>

</body>

</html>

按键 功能

A / D 左右移动

Space / W 跳跃

J / Z 攻击

R 死亡或通关后重开

相关推荐
YSL0701242 天前
贪吃蛇小游戏---古法编程
数据结构·游戏程序
全速向光3 天前
QQ宠物单机复刻版:重温青春记忆的开源怀旧版本
游戏程序·宠物
棉猴4 天前
玩游戏学Python12-布尔型与条件语句
java·前端·python·玩游戏·游戏编程·pgzero
棉猴4 天前
玩游戏学Python9-算术运算符应用1
python·游戏·pygame·玩游戏·游戏编程
棉猴4 天前
玩游戏学Python10-算术运算符应用2
开发语言·python·玩游戏·游戏编程
棉猴5 天前
玩游戏学Python1-Python和pgzero库的安装
python·pygame·玩游戏·pgzero·pgzrun
棉猴6 天前
玩游戏学Python2-IDLE与pgzero入门
开发语言·python·游戏·玩游戏·pgzero·pgzrun
棉猴6 天前
玩游戏学Python5-函数
开发语言·python·函数·玩游戏·游戏编程
棉猴8 天前
玩游戏学Python4-字符串
python·游戏·字符串·pygame·玩游戏