天地一!屋!大爱盟——飞机大战 at moonbit 开发心得

天地一!屋!大爱盟——飞机大战 开发心得

飞机大战 (支持一下下我们滴游戏吧)

\(moonbit-game-program\) (这是我们的仓库QWQ)

前言

梦开始的地方

在机缘巧合之下,我们通过学校的讲座,了解到有这样一个比赛——全新的,属于国人的编程语言\(Moonbit\)的编程比赛。基于(对比赛奖金的渴望)对新领域的好奇与探索,我们几个同学组成队伍,并迅速决定参加游戏赛道(毕竟还比较菜)。我们的队伍组成十分多样,其中一位成员来自中大,而其他成员均来自港科广。(也是抱到大腿了

初露端倪

对于一门全新编程语言的使用,大家不免有些陌生,更何况我们还需要利用它来完成项目。但是,


\(Moonbit\) 是一款由粤港澳大湾区数字经济研究院(福田)开发的全新部份开源编程语言。\(Moonbit\)被开发在一个称作「粤港澳大湾区」的现实世界,在这里被选中的同学将被赋予「参赛资格」,引导编程之力。你将扮演一位名为 「小白鼠」「参赛者」的神秘角色,在自由的「使用文档」中邂逅性格各异,能力独特的函数,和它们一起完成项目,赢得比赛的同时逐步找回「\(Moonbit\)」的真相。


(废话有点多了,QAQ...

初步地学习了用法后,我们开始设计游戏。基于160*160像素和技术力的限制,我们设想了一款与多年前很火的小游戏类似的“飞机大战”。游戏采用竖版模式,玩家控制的飞机可在可见区域内自由移动,两种类型的敌机从画面上方随机出现。玩家需要操纵飞机发射子弹击败敌机,同时躲避敌机的子弹与撞击,通过坚持更久的时间来赢得更高的分数。

逐渐听懂

我们最初的设想十分宏大,希望设计游戏开始界面、设置选项、三种敌机、不同的爆炸特效、背景音乐以及音效,甚至一些进阶的关卡(飞机轨迹变动之类的)。但是我们实际上手后才发现(抛开美术不谈),整体的工作量十分庞大,尤其是不同模块之间的兼容,容易导致各种各样的bug,我们直到10月25号才完成了第一版游戏,属于是那种可以玩但不多而且一堆bug的状态。

恍然大悟(抱歉,刚刚没有认出你,长官

我们在不断加东西的过程中,发现最大的问题不是debug以及代码量(年轻就是好),而是内存不足的问题!!!
当时还是很奇怪为什么没什么人询问内存相关的问题。应该说除了我们之外,其他绝大多数组好像并没有这个问题QAQ... 所以最后只能把很多设想一砍再砍(有些都写得差不多了呀)...
后面还是尽力优化了数组结构和一些不常用的函数,勉强塞进了一两句音效。
只能说我们对于\(Moonbit\)的内存结构以及变量类型的使用还不够熟练吧。在编写过程中,变量之间的强转用了很多(floatint之间还要转),占了不少空间。

这一趟旅程,虽然辛苦,但也值得。

具体实现

文件结构

project
│
├── src
│   ├── antity
│   │   ├── antity_memory.mbt
│   │   ├── struct_trait.mbt
│   │   └── struct.mbt
│   ├── bitset
│   │   └── bitset.mbt
│   ├── interface
│   │   ├── gaming.mbt
│   │   ├── interface_control.mbt
│   ├── time_cnt
│   │   ├── timer.mbt
│   └── top.mbt
  1. antity ~(单词打错了)~

    struct.mbt 中包含了关于游戏主体中飞机子弹的定义,struct_trait.mbt 包含了关于各种实体功能的具体实现,包括碰撞、血量和运动。

  2. bitset

    这个包里面实现了一个 \(moonbit\)\(minbitset\) (最后发现moonbit自带但是没有写在 core 里面,不知道为啥QWQ)。 这个主要是用于记录画面,节省空间。

  3. interface

    可以理解为游戏主体,用于实现多个界面的显示。但是由于前文提到的原因,实际上只写了两个界面(有点可惜)

  4. time_cnt

    计时器,用于统计帧数

  5. top

    这个就是游戏的入口啦

游戏逻辑

在游戏运行的主体部分分为四个阶段

1。移动
我们对每一个实体设定其标准坐标,自机移动的方向通过方向按键判断,其他实体的方向是生成时就确定了的,最后用三角函数计算标准坐标的位移。

以飞机位置更新

if (如果有按键被按下){
	update_self()
}
for i=aircraft.length()-1;i>=0;i=i-1{
	aircraft[i].update_pos()
}

2。判断碰撞
我们判断碰撞的方法是,枚举每一对实体,如果存在一个像素点同时在这对实体的碰撞箱中,则判定为相撞,并播放音乐。
如果飞机和子弹碰撞,计算血量变化。
如果自机和敌机碰撞或自机血量归零,则判定为游戏结束,播放特效。

以自机碰撞敌机为例

for i=aircraft.length()-1;i>=0;i=i-1{
	if(aircraft[i].crash(player_plane)){
		died.val=true
		aircraft.remove(i) |> ignore
	}
}

3。随机生成飞机和补给包(子弹箱和医疗箱)
随机数是通过moonbit内建的伪随机生成器生成的。
以生成医疗包为例

if(random.uint(limit =1000)<=3){
	generate_cartridge()
}

4。渲染画面
如果有特效,先运行特效逻辑(在后面说)
然后将每一个实体按照标准坐标计算出其碰撞箱的位置,并在其中填充像素。
4x。特效逻辑
我们的特效是通过四帧画面,每帧播放十六帧实现的,4种颜色足够展现飞机爆炸的场面了。

if(died.val){
//@wasm4.tone((200,201),@wasm4.ADSR::new(100),@wasm4.ADSRVolume::new(100),@wasm4.ToneFlag::new(channel=Noise,mode=Duty_1_2,pan=Center));
	if(updated.val.not()){
		player_Plane_effect.begin=@time_cnt.get_time()+1
		updated.val=true
	}
	else if(((@time_cnt.get_time()-player_Plane_effect.begin)/16).to_int()<4){
		@antity.showp(player_Plane_effect,(player_plane.pos.x-3).to_int(),(player_plane.pos.y).to_int(),((@time_cnt.get_time()-player_Plane_effect.begin)/16).to_int());
	}
	else{
	game_over()
	return;
		//Game Over
	}
}

图像绘制

由于我们发现 \(wasm4\) 中对图像的规格要求必须是 \(4\) 的倍数(\(2bits\))模式,所以我们自己重新写了一个来实现更高的自由度以及节省了一点点内存(这个真的很重要啊)。

pub fn show(self:Frame_effects,x:Int,y:Int)->Unit{
    let len=self.sprite.length()
    let mut index=0
    let mut posx=x
    let mut posy=y
    for i=0U;i<self.ylen;i=i+1{
        for e=0U;e<self.xlen;e=e+1{
            if((self.back_ground[index]==1||posx>=160||posx<0||posy<0||posy>low_y).not()){
                @wasm4.set_frame_buffer(get_index(posx.reinterpret_as_uint(),
                posy.reinterpret_as_uint()),
                self.sprite[index]+1
                )
            }
            index=index+1
            posx=posx+1
        }
        posx=x
        posy=posy+1
    }
}
// 画图函数

pub struct Frame_effects{
	xlen:UInt
	ylen:UInt
	sprite:@bitset.Bitset
	back_ground:@bitset.Bitset
}//画面结构体定义

空间开销(bitset)

(怎么说呢,这个耍了点小聪明但是有没完全达成目的)

我们最开始的图像使用 Int 类型存的(一个 Int 一个像素)。不用说,空间直接爆炸了。然后就去实现了一个 \(bitset\) 来存像素,效果很不错,在图像存储方面的提升极大。可能有人会问为啥不用 Bytes, 因为我去问开发人员时发现 Bytes 无法实现操作每一位QWQ。

let up_bit:Int=32;
pub struct Bitset{
  priv mut bits:FixedArray[UInt]
  priv mut top:Int//表示存了多少个pre_size
  per_size:Int
}

pub fn Bitset::length(self:Bitset)->Int{
  self.top
}

pub fn Bitset::new(per_size:Int,string:String)->Bitset{
  let ans:Bitset={bits:[],top:0,per_size}
  let mut block=-1
  let mut move=up_bit
  ans.top=string.length()/per_size
  ans.bits=FixedArray::make(ans.top,0)
  for i in string{
    if(move>=up_bit){
      block=block+1;
      move=0;
    }
    ans.bits[block]=ans.bits[block]|((i.to_int()-'0'.to_int()).to_uint()<<move)
    move=move+1
  }
  ans
}

pub fn op_get(self:Bitset,index:Int)->UInt{
  let indexx=index*self.per_size
  let mut block=indexx/up_bit
  let mut move=indexx-block*up_bit
  let mut ans:UInt=0;
  for i=0;i<self.per_size;i=i+1{
    if(move>=up_bit){
      block=block+1;
      move=0;
    }
    ans=ans|(((self.bits[block]>>move)&1)<<i)
    move=move+1;
  }
  ans
}
//bitset 具体实现

运动与碰撞

运动的实现是基于角度以及当前位置实现的,我们的每一实体拥有一个速度成员和一个方向成员(一个以X轴为0度,顺时针的一个角度),在每一帧的时候开始更新图像的基准位置(左上角)。

pub struct Aircraft{
  type_:Int
  pos:Location
  speed:Double
  mut dir:Double
  frame:Frame_effects
  mut hp:Double
  bullet_exit_point:Array[Bullet_exit_point]
}

pub fn Aircraft::update_pos(self:Aircraft)->Unit{
    let x=self.pos.x
    let y=self.pos.y
    self.pos.x=self.pos.x+self.speed*@math.cos(self.dir/180*@math.pi)
    self.pos.y=self.pos.y+self.speed*@math.sin(self.dir/180*@math.pi)
    if self.type_==3{
        if(self.is_out_partly()){
            self.pos.x=x
            self.pos.y=y
        }
    }
}

碰撞就比较简单粗暴啦,在位置更新后直接暴力检测每个像素是否重叠就 \(ok\)

pub fn crash(self:Bullet,obj:Aircraft)->Bool{
    let mut xs:Int=0
    let mut ys:Int=0
    let mut xo:Int=0
    let mut yo:Int=0
    let mut ans=false
    for i=self.frame.sprite.length()-1;i>=0&&ans.not();i=i-1{
        xs=i%self.frame.xlen.reinterpret_as_int()+self.pos.x.to_int()
        ys=i/self.frame.xlen.reinterpret_as_int()+self.pos.y.to_int()
        for j=obj.frame.sprite.length()-1;j>=0&&ans.not();j=j-1{
            xo=j%obj.frame.xlen.reinterpret_as_int()+obj.pos.x.to_int()
            yo=j%obj.frame.ylen.reinterpret_as_int()+obj.pos.y.to_int()
            if(xs==xo&&yo==ys) {ans=true}
        }
    }
    if(ans){
        obj.hp=obj.hp-self.harm
    }
    ans
}

后语

在这段充满挑战与收获的学习之旅中,我们深入掌握了 \(Moonbit\) 编程语言的简洁而现代的语法,这使我们得以运用这一强大的工具,将心中的游戏构想变为现实。通过 \(wasm4\) 引擎的开发实践,我们得以一窥那个充满活力、充满无限想象的 \(bit\) 游戏机时代。在 \(GitHub\) 这个协作无间的平台上,我们齐心协力,成功打造了这款名为“打飞机”的游戏,并选择将其开源,期待与全球开发者社区共享我们的创意与成果。

虽然 $Moonbit $是一门新兴的编程语言,它的确带来了一些挑战,比如类型强制转换的限制较多,可能会占用大量内存,一般函数不支持重载,以及语言本身存在的一些未定义行为。这些问题,连同官方文档的不清晰甚至错误,以及缺乏搜索关键词功能,都是我们在开发过程中遇到的问题。这些挑战提醒我们,即使是最先进的技术也在不断进化和完善中。

随着项目的圆满落幕,我们期待“打飞机”游戏不仅能为玩家带来欢乐时光,更能激发更多开发者对 $Moonbit $编程语言的热情。我们渴望与社区成员进行更深入的交流与合作,共同推动游戏开发技术的创新与发展。在此,我们向每一位贡献者的智慧与辛勤工作表示最深的感谢。让我们携手并进,满怀期待地在编程的未来道路上,继续创造更多令人心潮澎湃的杰作。

特别感谢:

小组成员:\(@DemonmasterLQX\) \(@WQR\) \(@DST683\) \(@Dzy\) \(@luoji12103 (D R)\)

来自 \(moonbit\) 官方的 \(helper\)\(@Tony~Fettes\)

posted @ 2024-11-12 00:27  轩Demonmaster  阅读(91)  评论(0编辑  收藏  举报