Skip to content
This repository has been archived by the owner on Feb 1, 2023. It is now read-only.

TransparentLC/project-striker

Repository files navigation

project-striker

build size lines

一个随便做做的弹幕射击游戏。

基本介绍

这是一个使用 pygame 制作的简单(?)弹幕射击游戏,目的是回避弹幕并击破敌机,在每一关的最后有一个比较强的 BOSS,击破后就算是过关了。

一共有 3 种机体和 4 个关卡,通关流程大概为 20 分钟。(通关演示/云玩家通道)

音乐和图片用的都是免费素材,具体请参见后面的借物表。没有剧情之类的设定。另外这个游戏里有不少借鉴东方 Project 的地方……

因为我之前几乎没有制作游戏的经验,所以代码可能十分混乱,某些设计可能也不是最佳实践,肥肠抱歉 ( >﹏<。)

(更详细的介绍请参见游戏内的说明)

如何运行

最简单的方式是直接下载使用 GitHub Actions 自动打包的,可以在 Windows/Linux 下运行的单个可执行文件或 macOS 下的 bundle。打包使用 PyInstaller 完成。

未登录 GitHub 的话,可以在这里下载:

和系统相关的说明:

  • Windows 版可能会被 Windows Defender 或其它杀毒软件报毒,属于误报。
  • Linux 版打包和测试是在 Ubuntu 20.04 上进行的,并没有测试在其他 Linux 发行版上是否可以运行。可能需要使用 LD_PRELOAD=/usr/lib64/libstdc++.so.6 等方式强制使用系统库。
  • macOS 版的 bundle 文件夹包含在一个 ZIP 文件内。
  • 打包后的可执行文件为 x64 架构。

也可以从源代码运行,不过稍微有些麻烦:

  • 需要 Python 3.9 或以上版本,使用之前的版本或许也可以运行,但我没有测试过。
  • 使用 pip install -r requirements.txt 安装依赖。
  • 参见这里下载字体。
  • 安装好 gccg++ ,然后执行 build-native.sh 编译 C/C++ 的函数库。
    • 对于 Windows 用户,已经准备了编译好的 DLL。
    • 对于 macOS 用户,在编译前需要执行 export libExtension=".dylib"
  • main.py 开始运行即可。

配置数据存储在用户目录下,删除即可完全初始化:

  • Windows:%HOMEPATH%/.striker
  • Linux:~/.striker

操作方法

  • 移动自机、在标题画面的菜单项中选择
  • LShift 使用低速移动
  • Z 射击(按住不放就可以连射)、确认
  • X 开启火力强化模式(参见游戏内的说明)、取消
  • P 暂停游戏,再按一下就会恢复
  • Esc 在游戏过程中返回标题画面

(想要改键或使用手柄?试试 PowerToysJoyToKey 之类的有按键映射功能的工具吧)

“太难了,根本不能通关” >_<

可以的。

所有的前半/后半道中,以及 BOSS 的每个攻击阶段(或者叫“身”)我都单独测试过了,在不丢失残机不使用火力强化的情况下都能通过,也就是说没有无解的弹幕设计

实在不能通关的话,损失所有残机满身疮痍之后还可以续关(最终 BOSS 战中除外),并且没有次数限制。不过续关之后:

  • 不再记录分数,以记录续关次数作为替代。
  • 不能见到最终 BOSS。

所以还是以不续关通关为目标努力吧!

借物表

可以改进的部分

做了当然会更好,虽然列在这里之后大概率就会一直摸了

  • 更多的性能优化(人家的弹幕游戏引擎随随便便就同屏几千甚至上万发,你这一千发好意思吗)
  • 完整的背景卷轴设计(素材不够)
  • 键位修改(暂时可以用改键工具替代)
  • ……

一些技术性较强的高级内容

对模组(MOD)的支持

通过加载自定义的模组文件,无需修改主程序代码即可实现替换图像、音频、关卡等功能。游戏会优先使用模组文件中相同路径的资源,如果无法找到则会继续使用自带的原版资源。

模组文件是包含以下目录的 tar 打包(请不要使用 gz、bz2、xz 等进行压缩),这些目录的具体用途可以参见“目录结构”部分:

  • assets
  • scriptfiles
  • sound

在启动游戏时,可以使用环境变量 STRIKER_MODDED_RESOURCE 指定模组文件的路径。

Replay 文件结构

Replay 文件分为文件头、校验码和按键数据三个部分。

文件头具体结构的伪代码:

struct ReplayHeader {
    uint8_t  magic[4];     // 固定为RPLY四个字符
    uint32_t version;      // 版本号,对于每个主程序版本有固定的默认值(游戏机制修改时会改变),与主程序对应版本号不同的replay可能无法正常播放
    uint64_t timestamp;    // 创建replay的时间戳
    uint8_t  name[8];      // 机签
    uint64_t seed;         // 随机种子(random.seed(version=2))
    uint64_t score;        // 游戏结束时的分数
    uint8_t  optionType;   // 自机类型
    uint8_t  missCount;    // MISS次数
    uint8_t  hyperCount;   // 火力强化使用次数
    uint8_t  bonusCount;   // 完美击破奖励次数
    uint32_t unused;       // 未使用
}

校验码计算规则:hmac_sha256(msg=文件头 + 解压后的按键数据, key=???)。文件头和校验码共计 80 字节,

按键数据保存时使用 lzma.compress(format=lzma.FORMAT_ALONE) 压缩。在原始数据中每一帧用一个 uint8_t 表示,每个位表示一个按键是否有按下:

  • 1 << 0
  • 1 << 1
  • 1 << 2
  • 1 << 3
  • 1 << 4 LShift
  • 1 << 5 Z
  • 1 << 6 X
  • 1 << 7 C(未使用)

通关流程大概为 20 分钟,即 20x60x60=72000f。由于存在压缩,文件大小理论上不会大于 70 KB。

关于判定和同屏弹幕量

自机、敌机及弹幕均使用圆形判定,对于 BOSS 之类的较大的敌机则会使用多个圆以尽可能地覆盖敌机图像。

BOSS 战中同屏弹幕量一般控制在 250 左右,但在保持 FPS 稳定在 60 左右(不出现处理落)的前提下,同屏弹幕的极限数量可以达到 1000 左右。

可以考证的其他一些弹幕射击游戏的同屏弹幕量:

  • 1998 年的长空超少年,同屏弹幕量一般不超过 300(STAGE 5B 的 BOSS 战,参见 PS4 版显示的计数)
  • 2003 年的绊地狱,同屏弹幕量一般不超过 150(里二周目 STAGE 5 光翼战,参见 PS4 版显示的计数,这两个的原作是街机游戏)
  • 2002 年的东方红魔乡,同屏弹幕上限 640
  • 2007 年的东方风神录,同屏弹幕上限 2000(参见这里,这两个是使用 C++ 和 DirectX 制作的 PC 游戏)

由于 pygame 仅使用软件渲染,并且我没有使用多进程,所以只要 CPU 主频足够这个数值的差别就不会很大,显卡性能也不会产生影响。

Python 写的东西,还想要什么性能 (╯‵□′)╯︵┻━┻

目录结构

project-striker
│  .gitignore
│  build-info.txt # 在打包时记录打包时间及对应的commit信息
│  build.ps1 # 打包脚本
│  build.sh # 打包脚本
│  icon.ico
│  LICENSE
│  main.py # 入口文件
│  README.md
│  requirements-build.txt
│  requirements.txt
│
├─.github
│  └─workflows
│          build.yml # Github Actions 自动打包脚本
│
├─.vscode
│      launch.json
│
├─assets # 图片素材
│      ...
│
├─font # 字体
│      README.md
│      SourceHanSerifSC-Medium.otf
│
├─lib
│  │  constants.py # 部分常数
│  │  debug.py # 测试用函数,目前只有显示弹幕判定大小
│  │  font.py # 字体渲染,主要是处理多行文本
│  │  globals.py # 全局变量
│  │  scroll_map.py # 背景卷轴
│  │  sound.py # 背景音乐和音效播放
│  │  stg_overlay.py # 游戏主界面上提示分数/残机奖励等等的图层
│  │  utils.py # 一些工具函数及各种插值函数
│  │  __init__.py
│  │
│  ├─bullet
│  │  enemy_bullet.py # 敌机弹幕相关
│  │  player_bullet.py # 自机弹幕相关
│  │  __init__.py
│  │
│  ├─scene # 游戏中的各个界面
│  │  config.py
│  │  manual.py
│  │  result.py
│  │  select_option.py
│  │  stg.py
│  │  title.py
│  │  __init__.py
│  │
│  ├─script_engine # 敌机行动脚本和关卡脚本的解析
│  │  enemy.py
│  │  README.md
│  │  stage.py
│  │  __init__.py
│  │
│  └─sprite # 各种活动块相关
│     debris.py # 击破敌机后的碎片效果
│     enemy.py # 敌机
│     explosion.py # 击破敌机后的爆炸效果
│     item.py # 道具
│     option.py # 自机的子机
│     player.py # 自机
│     __init__.py
│
├─native # 以C/C++代码编译的函数库
│     ...
│
├─psd # 背景图片的源文件
│     ...
│
├─scriptfiles
│  ├─enemy # 敌机脚本
│  │      ...
│  │
│  ├─map # 背景卷轴内容
│  │      ...
│  │
│  └─stage # 关卡脚本
│         ...
│
├─sound
│  ├─bgm # 背景音乐
│  │      ...
│  │
│  └─sfx # 音效
│         ...
│
└─tool # 其他的工具脚本
      generate-font-subset.py # 生成子集化字体
      map-editor.html # 背景卷轴编辑器
      script-tag.py # 在编写脚本时生成随机的标签
      webp-lossless.py # 将图片无损转换为webp