1 小时学会用 MoonBit 开发马里奥游戏 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Moonbit
V2EX    编程

1 小时学会用 MoonBit 开马里奥游戏

  •  
  •   Moonbit 2023-12-06 18:21:45 +08:00 901 次点击
    这是一个创建于 673 天前的主题,其中的信息可能已经有所发展或是发生改变。

    嘿大家好!今天来讲讲一个好玩的游戏马里奥游戏!

    相信不少 80 、90 后的朋友们在小时候都玩过马里奥游戏,对那个戴着红色帽子、穿着蓝色工装背带裤的马里奥叔叔念念不忘。

    在这里插入图片描述

    这款游戏自 1985 年面世以来,就以简单易上手和丰富有趣的情节关卡设计,迅速俘获全球玩家的心。

    今天,让我们重拾那份童年的情怀~如果你的童年也曾被那魔性的 “灯灯灯灯灯灯灯”旋律洗脑,那就一起来追忆那些美好时光吧!让我们动起手来,用 MoonBit 创造一个属于自己的“马里奥游戏”吧!

    基本元素

    首先,让我们来梳理马里奥游戏中的基本元素。

    在游戏中,一共存在四种可以互相交互的对象,它们分别是玩家、敌人、物品、砖块。

    每一种对象都有各自的分类,比如玩家按尺寸来说分为大小,按状态来说分为站立、奔跑、跳跃、蹲下。

    我们依次定义这四种对象,并且用一个枚举类型来统一它们:

    enum PlayerSize { Small Large } enum Player { Standing Jumping Running Crouching } enum Item { Mushroom Coin } enum Enemy { Goomba GKoopa RKoopa GKoopaShell RKoopaShell } enum Block { QBlock(Item) QBlockUsed Brick UnBBlock Cloud Panel Ground } enum Spawn { Player(PlayerSize, Player) Enemy(Enemy) Item(Item) Block(Block) } 

    除了这四种基本对象之外,还有一种特殊的对象并不与其他对象进行交互,而只是一段固定的动画,例如砖块碎裂后四散的碎片,敌人死去后飘起的分数,我们也为它们定义一个枚举类型。

    enum Part { GoombaSquish BrickChunkL BrickChunkR Score100 Score200 Score400 Score800 Score1000 Score2000 Score4000 Score8000 } 

    最后,每个对象都有自己的坐标;玩家和敌人都有各自的朝向,向左或者向右;对象之间唯一的交互方式是碰撞,而碰撞则分为上下左右四个方向。我们依次定义这些概念。

    struct XY { mut x : Double mut y : Double } enum Dir1d { Left Right } enum Dir2d { North South East West } 

    Sprite

    每个对象都需要在能在背景图片之上显示属于自己的动画,例如行走中的马里奥就由四帧动画组成。

    在这里插入图片描述

    我们用 Image 类型来表示由 Javascript 运行时提供的图片,每个不同状态的对象都需要在对应的图片上裁剪出属于自己的部分。

    此外,在判断碰撞时,每个对象在逻辑上也同样是一个个的方块,这种逻辑上的方块与对象图片所在的方块并不总是相同。例如对于乌龟来说,逻辑方块不包含头部。因此我们要分开定义两种方块。

    在这里插入图片描述

    struct SpriteParams { max_frames : Int max_ticks : Int img_src : Image frame_size : (Double, Double) src_offset : (Double, Double) bbox_offset : (Double, Double) bbox_size : (Double, Double) loop : Bool } struct Sprite { mut params : SpriteParams frame : Ref[Int] ticks : Ref[Int] mut img : Image } 

    如果对各个参数的作用有疑惑,可以以一个具体的例子作为参考,下面是砖块的例子。

    fn make_block(block : Block) -> SpriteParams { match block { Brick => setup_sprite_(block_, 5, 10, (16.0, 16.0), (0.0, 0.0)) QBlock(_) => setup_sprite_(block_, 4, 15, (16.0, 16.0), (0.0, 16.0)) QBlockUsed => setup_sprite_(block_, 1, 0, (16.0, 16.0), (0.0, 32.0)) UnBBlock => setup_sprite_(block_, 1, 0, (16.0, 16.0), (0.0, 48.0)) Cloud => setup_sprite_(block_, 1, 0, (16.0, 16.0), (0.0, 64.0)) Panel => setup_sprite_(panel_, 3, 15, (26.0, 26.0), (0.0, 0.0)) Ground => setup_sprite_(ground, 1, 0, (16.0, 16.0), (0.0, 32.0)) } } 

    在这里插入图片描述

    在这里插入图片描述

    对于砖块来说,逻辑方块总是等于图片方块,因此在我们的构造函数中,我们只需要传入对象所在的图片、对象图片的帧数、每一帧持续的时间、对象图片的大小、对象图片的第一帧在整张图片中的位置。

    碎片

    接下来我们来处理一个相对简单的部分,即不需要和其他对象进行交互的碎片。

    除了持续时间之外,每一个碎片都要记录自己的位置、速度和加速度,例如碎裂的砖块会做抛物运动,而敌人死后飘出的分数则做匀速直线运动。

    struct ParticleParams { sprite : Sprite lifetime : Int } struct Particle { params : ParticleParams pos : XY vel : XY acc : XY mut kill : Bool mut life : Int } 

    生成一个碎片之后,我们只要在每一帧结束之后独立地更新它的状态就可以了,不需要考虑交互的问题。

    fn update_vel(self : Particle) { self.vel.x = self.vel.x + self.acc.x self.vel.y = self.vel.y + self.acc.y } fn update_pos(self : Particle) { self.pos.x = self.pos.x + self.vel.x self.pos.y = self.pos.y + self.vel.y } fn process(self : Particle) { self.life = self.life - 1 if self.life == 0 { self.kill = true } self.update_vel() self.update_pos() } 

    对象

    而对于能够相互碰撞的对象来说,处理起来则要复杂一些。

    首先,我们需要为每个对象定义最基本的属性,例如位置、速度、编号、状态。

    struct ObjectParams { has_gravity : Bool speed : Double } struct Object { params : ObjectParams pos : XY vel : XY id : Int mut jumping : Bool mut grounded : Bool mut dir : Dir1d mut invuln : Int mut kill : Bool mut health : Int mut crouch : Bool mut score : Int } 

    接下来我们用一个枚举类型来统一四种基本对象。

    enum Collidable { Player(PlayerSize, Sprite, Object) Enemy(Enemy, Sprite, Object) Item(Item, Sprite, Object) Block(Block, Sprite, Object) } 

    在每一帧结束之后,除了独立地更新每个对象,我们还要处理它们之间的交互。

    交互只会通过碰撞发生,所以我们首先需要判断两个对象之间是否发生了碰撞,以及碰撞的方向。

    注意代码中的 col_bypass 过滤掉了互不影响的对象,比如敌人和硬币之间碰撞时可以不做任何处理,只是简单的互相穿过。

    fn check_collision(c1 : Collidable, c2 : Collidable) -> Option[Dir2d] { let b1 = get_aabb(c1) let b2 = get_aabb(c2) let o1 = get_obj(c1) if col_bypass(c1, c2) { Option::None } else { let vx = b1.center.x - b2.center.x let vy = b1.center.y - b2.center.y let hwidths = b1.half.x + b2.half.x let hheights = b1.half.y + b2.half.y if abs(vx) < hwidths && abs(vy) < hheights { let ox = hwidths - abs(vx) let oy = hheights - abs(vy) if ox >= oy { if vy > 0.0 { o1.pos.y = o1.pos.y + oy Option::Some(Dir2d::North) } else { o1.pos.y = o1.pos.y - oy Option::Some(Dir2d::South) } } else if vx > 0.0 { o1.pos.x = o1.pos.x + ox Option::Some(Dir2d::West) } else { o1.pos.x = o1.pos.x - ox Option::Some(Dir2d::East) } } else { Option::None } } } 

    在判断完碰撞关系之后,我们开始处理对象之间的交互。

    玩家在敌人之上、玩家在砖块之下、玩家吃到金币,不同的事件会触发不同的处理函数,因此下面的判断函数不可避免地稍显复杂,它细致地对不同对象之间的交互进行了分类处理。

    fn process_collision(dir : Dir2d, c1 : Collidable, c2 : Collidable, state : St) -> (Option[Collidable], Option[Collidable]) { match (c1, c2, dir) { (Player(_, _, o1), Enemy(typ, s2, o2), South)| (Enemy(typ, s2, o2), Player(_, _, o1), North) => player_attack_enemy( o1, typ, s2, o2, state, ) (Player(_, _, o1), Enemy(t2, s2, o2), _)| (Enemy(t2, s2, o2), Player(_, _, o1), _) => enemy_attack_player( o1, t2, s2, o2, ) (Player(_, _, o1), Item(t2, _, o2), _)| (Item(t2, _, o2), Player(_, _, o1), _) => match t2 { Mushroom => { dec_health(o2) if o1.health == 2 { () } else { o1.health = o1.health + 1 } o1.vel.x = 0.0 o1.vel.y = 0.0 update_score(state, 1000) o2.score = 1000 (None, None) } Coin => { state.coins = state.coins + 1 dec_health(o2) update_score(state, 100) (None, None) } } (Enemy(t1, s1, o1), Enemy(t2, s2, o2), dir) => col_enemy_enemy( t1, s1, o1, t2, s2, o2, dir, ) (Enemy(t1, s1, o1), Block(t2, _, o2), East)| (Enemy(t1, s1, o1), Block(t2, _, o2), West) => match (t1, t2) { (RKoopaShell, Brick) | (GKoopaShell, Brick) => { dec_health(o2) reverse_left_right(o1) (None, None) } (RKoopaShell, QBlock(typ)) | (GKoopaShell, QBlock(typ)) => { let updated_block = evolve_block(o2) let spawned_item = spawn_above(o1.dir, o2, typ) rev_dir(o1, t1, s1) (Some(updated_block), Some(spawned_item)) } (_, _) => { rev_dir(o1, t1, s1) (None, None) } } (Item(_, _, o1), Block(_), East) | (Item(_, _, o1), Block(_), West) => { reverse_left_right(o1) (None, None) } (Enemy(_, _, o1), Block(_), _) | (Item(_, _, o1), Block(_), _) => { collide_block(true, dir, o1) (None, None) } (Player(t1, _, o1), Block(t, _, o2), North) => match t { QBlock(typ) => { let updated_block = evolve_block(o2) let spawned_item = spawn_above(o1.dir, o2, typ) collide_block(true, dir, o1) (Option::Some(spawned_item), Option::Some(updated_block)) } Brick => if t1 == Large { collide_block(true, dir, o1) dec_health(o2) (None, None) } else { collide_block(true, dir, o1) (None, None) } Panel => { state.game_over = true game_win() (None, None) } _ => { collide_block(true, dir, o1) (None, None) } } (Player(_, _, o1), Block(t, _, _), _) => match t { Panel => { state.game_over = true game_win() (None, None) } _ => match dir { South => { state.multiplier = 1 collide_block(true, dir, o1) (None, None) } _ => { collide_block(true, dir, o1) (None, None) } } } _ => (None, None) } } 

    对于相对简单的情况,例如到达终点游戏结束,我们直接在上面的函数中处理掉了。而对于更多的情况,我们在专门的函数中处理,例如下面的函数处理了玩家和砖块碰撞的情况。我们可以看到,发生碰撞之后,玩家相应方向上的速度降低到零,其余的属性也做出相应的改变。

    fn collide_block(check_x : Bool, dir : Dir2d, obj : Object) { match dir { North => { obj.vel.y = -0.001 } South => { obj.vel.y = 0.0 obj.grounded = true obj.jumping = false } East | West => if check_x { obj.vel.x = 0.0 } } } 

    完整代码

    以上就是 MoonBit 写马里奥游戏的简要介绍,完整的代码可以访问我们的在线 IDE 。在 MoonBit 实时编程环境中,你可以灵活调整马里奥的跳跃高度,实时创建多个马里奥角色,探索多重乐趣。此外,你还能实时调整游戏结束的逻辑,非常适合通过实践来理解和学习。

    在线 IDE 链接: https://www.moonbitlang.cn/gallery/mario/

    详细内容戳: https://mp.weixin.qq.com/s/pyJo8xcZ89o-ov0umwXW4A

    1 条回复    2023-12-06 18:32:34 +08:00
    AoEiuV020JP
        1
    AoEiuV020JP  
       2023-12-06 18:32:34 +08:00
    龟头没有碰撞箱?原版马里奥也是这样吗,新版是不是都这样的,
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     3573 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 26ms UTC 04:16 PVG 12:16 LAX 21:16 JFK 00:16
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86