成就中心 DouJin Terraria! 社区服务 统计排行 帮助
  • 6477阅读
  • 8回复

[心得交流]LuaSTG试玩记载

发帖
179
信仰
0
蓝点
171
符卡
0
/前言
1.这个大概可以关于Lua的协程与闭包的用途的最简单介绍。专门的文章比较难写,于是一边实践,一边记下来一点东西。
2.这里面用到了math.random,于是在stage开始处调用math.randomseed(0)将种子强制归0,以保证录像播放。另外比较好的方法是在replay中保存seed,只是我懒得研究这个问题了
3.里面创建enemy的方法是coroutine中将闭包返给主进程执行是由于我有些问题搞不定,反正直接调用New就会悲剧= =
4.虽然这是娱乐向的超级短,也欢迎大家打录像玩~


/开始
Resty想要自己创建一个关卡,于是在mod下创建了一个叫做dream的文件夹,用作这次的设计目录,然后再在里面创建一个script目录,用来放置脚本,最后创建root.lua,开始编写我们的内容。

首先,我想要有一个开始的菜单界面,于是先把debug下的root.lua中的statge_init的全部相关内容copy过来,这样,菜单就有了= =

然后学着stage_demo的方法创建stage_dream:
  1. stage_dream = stage.New(false)
  2. function stage_dream:init()
  3.     InitVar()
  4.     New(_G[lstg.var.player_name])
  5.     New(bamboo_background)
  6.     
  7.     self.process = coroutine.create(stage_of_dream)
  8. end
  9. function stage_dream:frame()
  10.     flag, func = coroutine.resume(self.process)
  11.     if not flag then
  12.         stage.Set('none','none','stage_init')
  13.     elseif func then
  14.         func()
  15.     end
  16. end


render部分直接抄过来没有修改。


解释一下,我希望通过coroutine来管理我的stage进程,于是在init的时候创建了self.process, 然后在每一帧的时候remsume一下~。
我希望把stage的内容写到单独的文件中,于是创建了一个文件stage.lua
在root.lua开头加上Include’stage.lua’,另外还有一些函数会经常使用,于是再创建一个文件util.lua,在root.lua开头加上Include’util.lua’
然后在stage.lua中编写关卡
  1. function stage_of_dream()
  2.     wait(300)
  3. end

现在没什么事好做,于是先等待300帧,然后结束。
wait函数在util.lua中编写:
  1. function wait(count)
  2.     if count > 0 then
  3.         coroutine.yield()
  4.         wait(count - 1)
  5.     end
  6. end

传入要等待的帧数,就会等待这么多帧恩~

接着我们要考虑加入enemy
首先加入两个概念,mover和firer。mover是一个函数,其作用是控制一个对象的运动轨迹,firer是另一个函数,其作用是控制enemy的开火情况,于是我们创建一个使用mover和firer的enemy对象:
  1. denemy=Class(girl)
  2. function denemy:init(id, hp, mover, firer, protect_time, drop)
  3.     girl.init(self, id, hp)
  4.     self.mover = mover
  5.     self.firer = firer
  6.     self.ptime = protect_time
  7.     self.protect = true
  8.     if drop then
  9.         self.drop = drop
  10.     end
  11. end
  12. function denemy:frame()
  13.     girl.frame(self)
  14.     self.x, self.y = self.mover()
  15.     self.firer(self.x, self.y)
  16.     if self.ptime > 0 then
  17.         self.ptime = self.ptime - 1
  18.         if self.ptime <= 0 then
  19.             self.protect = false
  20.         end
  21.     else
  22.         if self.x < -300 or self.x > 300 or self.y < -250 or self.y > 250 then
  23.             Del(self)
  24.         end
  25.     end
  26. end

再解释一下子,mover是一个返回三个值(x, y, ang)的函数,firer是一个接受(x, y)作为参数的函数,作为子弹的发射点。 frame函数中, 其调用mover来决定自己的坐标,调用firer来控制是否开火。mover中的ang是用来控制子弹的朝向的,在enemy处理的时候不需要考虑。

接下来,我们先实现一个简单的mover,直线移动:
  1. function mvdirect(x, y, vx, vy)
  2.     local ang = math.atan2(vy, vx)
  3.     return function()
  4.         x = x + vx
  5.         y = y + vy
  6.         return x, y, ang
  7.     end
  8. end
  9. function mvdirecta(x, y, v, ang)
  10.     local vx = math.cos(ang) * v
  11.     local vy = math.sin(ang) * v
  12.     return function()
  13.         x = x + vx
  14.         y = y + vy
  15.         return x, y, ang
  16.     end
  17. end

这两个函数创建的是同样的直线移动的mover,只是传入的参数有差异。

再创建一个不开火的firer
  1. function fire_null()
  2.     return function(x, y) end
  3. end

注意这里的函数都是mover和firer的构造器,它们的返回值是一个函数,就是对应的mover / firer
于是我们可以在我们的stage中加入第一批敌人:
  1. for i = 1, 6 do
  2.         coroutine.yield(function () New(denemy, 1, 1, mvdirect(-100, 240, 0, -2), fire_null(), 45, {1, 0, 0}) end)
  3.         wait(20)
  4. end
  5. wait(120)
  6. for i = 1, 6 do
  7.         coroutine.yield(function () New(denemy, 1, 1, mvdirect(100, 240, 0, -2), fire_null(), 45, {1, 0, 0}) end)
  8.         wait(20)
  9. end


这波敌人的mover是从屏幕上方以速度vy=-2向下运动~
可以先进游戏试试,可以打掉这两波敌人了,它们都会掉落1个红点。
注意到我们这两波敌人,除了mover的x值不同外,其它的都是一样的,我们可以为其这一公共的特性建立抽象,创建函数
  1. local function egroup1(x)
  2.     for i = 1, 6 do
  3.         coroutine.yield(function () New(denemy, 1, 1, mvdirect(x, 240, 0, -2), fire_null(), 45, {1, 0, 0}) end)
  4.         wait(20)
  5.     end
  6. end

那么我们的stage可以简单的写成:
  1. function stage_of_dream()
  2.     wait(300)
  3.     egroup1(-100)
  4.     wait(120)
  5.     egroup1(100)
  6.     wait(600)
  7. end


然后我们再给这些敌人加上一些子弹:
创建firer:
  1. function firer_small()
  2.     local cnt = 40
  3.     return function (x, y)
  4.         cnt = cnt - 1
  5.         if cnt <= 0 then
  6.             ball_to_player(x, y, 4)
  7.             cnt = math.random(30) + 40
  8.         end
  9.     end
  10. end

cnt是控制两发子弹之间的间隔,当cnt = 0时发射子弹,然后再随机生成下个子弹发射的间隔。
其中ball_to_player是朝player发射一个ball子弹:
  1. function ball_to_player(x, y, speed)
  2.     New(rbullet, ball_mid, 2, true, mvdirecta(x, y, speed, math.atan2(lstg.player.y-y, lstg.player.x-x)))
  3. End

这样我们修改egroup1中的创建函数,更换相应的firer,即可让这些敌人零散的发射一点子弹。

接下来我们再来创造下一波敌人,这波敌人我希望他们横着运动,这点我们的mvdirect本身就能实现,然后发射更加密集和快速的自机狙:
  1. local function egroup2 ()
  2.     for i = 1, 12 do
  3.         coroutine.yield(function () New(denemy, 2, 2, mvdirect(-200,165,2,0), fire_more(), 30, {0, 1, 0}) end)
  4.         wait(15)
  5.         coroutine.yield(function () New(denemy, 3, 2, mvdirect(200,145,-2,0), fire_more(), 30, {0, 0, 1}) end)
  6.         wait(20)
  7.     end
  8. end

这里循环中创建了两个敌人,分别从屏幕左侧和右侧进入屏幕中。
然后我们需要实现fire_more()
如下:
  1. function fire_more()
  2.     local cnt = 10
  3.     return function (x, y)
  4.         cnt = cnt - 1
  5.         if cnt <= 0 then
  6.             ball_to_player(x, y, 5)
  7.             cnt = math.random(10) + 10
  8.         end
  9.     end
  10. end

很容易发现,fire_little和fire_more的形式非常接近,不同的地方只有子弹速度,和时间间隔,于是我们可以将其抽象为fire_some:
  1. function fire_some(init, det, v)
  2.     local cnt = init
  3.     return function (x, y)
  4.         cnt = cnt - 1
  5.         if cnt <= 0 then
  6.             ball_to_player(x, y, v)
  7.             cnt = math.random(det) + init
  8.         end
  9.     end
  10. end
  11. function fire_little()
  12.     return fire_some(40, 30, 4)
  13. end
  14. function fire_more()
  15.     return fire_some(10, 10, 5)
  16. end

最后修改我们的stage,加入egroup2就可以观察效果了~

接下来我们创建一个复杂一点的mover:
  1. function mvcons(t, car, cdr)
  2.     return function()
  3.         local x, y, ang = car()
  4.         if t >= 0 then
  5.             t = t - 1
  6.             if t < 0 then
  7.                 car = cdr(x, y, ang)
  8.             end
  9.         end
  10.         return x, y, ang
  11.     end
  12. end

mvcons的作用是一个连接符,它表示:先使用car这个mover运作t帧,之后用cdr来构造另一个mover,使之控制接下来的进程。
Cdr是一个mover的构造器,其应当接受3个参数:x, y, ang为上一个mover最后一次返回的3个参数,如果你需要考虑到运动的连贯性,则需要这些参数来构造下一个mover(虽然我都没用过ang参数,但当你制作一个之前沿曲线运动,最后朝切线方向飞出的mover时很可能会用的哦)。

我们利用mvcons可以构造一点复杂的效果:
于是我接下来创建一个大型一点的敌人:
  1. local function ebig1()
  2.     coroutine.yield(
  3.         function()
  4.             New( denemy,
  5.                 9,
  6.                 120,
  7.                 mvcons(40,
  8.                     mvdirect(0, 250, 0, -3),
  9.                     function (x, y)
  10.                         return mvcons(600,
  11.                                     function() return x, y end,
  12.                                     function(x, y) return mvdirect(x, y, 0, 4) end)
  13.                     end
  14.                 ),
  15.                 fire_delay(40, fire_wait(10, fire_around(12, 3.5))),
  16.                 90,
  17.                 {24, 6, 6})
  18.         end
  19.     )
  20. end

注意到这回的敌人的mover是由我们定义的多个基本的mover拼合而成,其粘合剂就是mvcons,另外这个表达式中声明了一个临时的mover即function() return x, y end,其表示呆在原地不动~
那么这里的mover就表示先从(0, 250)向下移动40帧,然后原地停止600帧,然后再向上运动离开屏幕~
我在stage中再加入这一段:
  1. ebig1()
  2. egroup2()
  3. wait(650)

将这个大型敌人和group2混在一起,作为这个关卡的最后一段。
然后我们来解释ebig1构造中的firer。
我们先在firer中加入下面一些内容:
1.    fire_delay : 它接受另一个firer, 并使得该firer延迟cnt帧后才发生作用。
2.    fire_wait : 它接受另一个firer, 并使得该firer在每cnt帧被调用一次。
其实现如下:
  1. function fire_delay(cnt, firer)
  2.     return function(x, y)
  3.         if cnt > 0 then
  4.             cnt = cnt - 1
  5.         else
  6.             firer(x, y)
  7.         end
  8.     end
  9. end
  10. function fire_wait(cnt, firer)
  11.     local delay = cnt
  12.     return function (x, y)
  13.         delay = delay - 1
  14.         if delay <= 0 then
  15.             delay = cnt
  16.             firer(x, y)
  17.         end
  18.     end
  19. end

而fire_around只是一个普通的朝着全方位发射固定弹的发射器
  1. function fire_around(n, v)
  2.     local base = 0
  3.     local dt = 2 * math.pi / n
  4.     return function (x, y)
  5.         base = base + 0.1
  6.         for i = 1, n do
  7.             ball_to_angle(x, y, v, base + i * dt)
  8.         end
  9.     end
  10. end

其中ball_to_angle的实现是:
  1. function ball_to_angle(x, y, speed, angle)
  2.     New(rbullet, big_arrow, 3, true,  mvdirecta(x, y, speed, angle))
  3. end

而ebig1的firer即fire_delay(40, fire_wait(10, fire_around(12, 3.5)))
可以解释为 40帧后开始发射,每隔10帧发射一次,每次朝着周围发射12发子弹,弹速为3.5


[ 此帖被resty在2011-03-20 10:07重新编辑 ]
本帖最近评分记录: 1 条评分 蓝点 +10 隐藏
franniss 蓝点 +10 2011-03-20 原创心得奖励
发帖
179
信仰
0
蓝点
171
符卡
0
只看该作者 沙发  发表于: 2011-03-19
于是我们道中就先设计到这里好了,接下来制作boss战:
Boss战嘛,还是接着抄debug的代码,不过我们先给出spellcard的定义:
命名莫在意了,card.New是创建一张符卡,我不希望关心其细节,于是将接口转换成了:
name – 符卡名:空串为非符
hp – 不解释
t – 计时器
drop – 掉落的道具
mover / firer同之前.
  1. function rdemo(name, hp, t, drop, mover, firer)
  2.     local demo=card.New(name, t / 8, t / 3, t, hp, name=="" , drop)
  3.     function demo:frame()
  4.         self.x, self.y = mover()
  5.         firer(self.x, self.y)
  6.     end
  7.     
  8.     return demo
  9. end

修正下,name==""逻辑上因该是name~="",嘛不过对整体影响不大么我就没改 = =
然后这回的boss被我偷懒,写成了这样:
  1. boss_demo=Class(boss)
  2. function boss_demo:init()
  3.     boss.init(self,{
  4.         rdemo('', 250, 30, {14, 0, 14},
  5.             mvcons(60, mvdirect(0, 250, 0, -3),
  6.                 function (x, y)
  7.                     return function() return x, y end
  8.                 end),
  9.             fire_delay(60,
  10.                 fire_union(fire_wait(10, fire_around_n(3, 3, 0.08, 5)),
  11.                     fire_wait(20, fire_around_n(4, 2, -0.05, 2))))),
  12.                     
  13.         
  14.         rdemo('demo_sc', 300, 45, {14, 0, 14},
  15.             function() return 0, 128 end,
  16.             fire_delay(120,
  17.                 fire_union(
  18.                     fire_wait(90, fire_union(
  19.                         fire_around_sc(24, 1, 5),
  20.                         fire_around_sc(18, 2, 3))),
  21.                     fire_wait(45, fire_around_n(4, 2, 0.4, 4)))))
  22.     })
  23.     self.x=0 self.y=256
  24. end
  25. function boss_demo:defeat()
  26.     boss_defeat = true
  27. end

这里直接设置了两个符卡,我们一个一个来分析:
第一个rdemo:
非符,hp=250, time=30, drop={14, 0, 14}
Mover是先下移60f,然后呆着不动~
Firer处引入了一个新的算子firer_union, 其可以让boss同时使用两种不同类型的firer:
  1. function fire_union(f1, f2)
  2.     return function (x, y)
  3.         f1(x, y)
  4.         f2(x, y)
  5.     end
  6. end

fire_around_n是朝四周发射多束的固定弹
  1. function fire_around_n(n, v, det, k)
  2.     local base = 0
  3.     local dt = 2 * math.pi / n
  4.     return function (x, y)
  5.         base = base + det
  6.         for i = 1, n do
  7.             ball_to_angle_multi(x, y, v, base + i * dt, 2, 0.5, k)
  8.         end
  9.     end
  10. end

其中ball_to_angle_multi朝着某个角度发射复数子弹:
  1. function ball_to_angle_multi(x, y, speed, angle, n, range, k)
  2.     local dt = range / 2 / n
  3.     for i = - n, n do
  4.         New(rbullet, big_arrow, k, true,  mvdirecta(x, y, speed, angle + i * dt))
  5.     end
  6. end


其形成了诸如这样的子弹:


第二个符卡,加入了fire_around_sc
实现如下:
  1. function fire_around_sc(n, k, v0)
  2. local dt = 2 * math.pi / n
  3.     return function (x, y)
  4.         local base = math.random()
  5.         for i = 1, n do
  6.             ball_to_angle_sc(x, y, base + i * dt, k, v0)
  7.         end
  8.     end
  9. end

其还是发射整圈的子弹,不过这回子弹的mover比较特殊:
  1. function ball_to_angle_sc(x, y, ang, k, v0)
  2.     New(rbullet, square, k, true,
  3.         mvcons(120, mvdirecta_slowd(x, y, v0, ang, 1 / 60),
  4.             function (x, y)
  5.                 return
  6.                 mvcons(120, mvdirecta_slowd(x, y, 5, math.atan2(lstg.player.y-y, lstg.player.x-x), 1 / 60),
  7.                     function (x, y)
  8.                         return mvdirecta(x, y, 3, math.atan2(lstg.player.y-y, lstg.player.x-x))
  9.                     end)
  10.             end)
  11.     )
  12. end

ball_to_angle_sc创建的子弹使得其mover为:朝原方向减速运动120秒(实际上60秒时即停下),然后朝player减速运动,之后再变向朝player来一下(这不是灵梦的那啥么= =)。
最后, 这里用到的mvdirecta_slowd为:
  1. function mvdirecta_slowd(x, y, v, ang, ds)
  2.     local vx = math.cos(ang) * v
  3.     local vy = math.sin(ang) * v
  4.     local scale = 1
  5.     return function()
  6.         if scale > 0 then
  7.             scale = scale - ds
  8.         else
  9.             scale = 0
  10.         end
  11.         
  12.         x = x + vx * scale
  13.         y = y + vy * scale
  14.         return x, y, ang
  15.     end
  16. end

该spell的简易效果

我们最后只要在stage里加入:
  1. coroutine.yield( function () New(boss_demo) end )
  2. wait_boss()

这个简单的关卡就完工了

其中wait_boss为
  1. function wait_boss()
  2.     if not boss_defeat then
  3.         coroutine.yield()
  4.         wait_boss()
  5.     else
  6.         boss_defeat = false
  7.     end
  8. end

该关的全部内容:
  1. function stage_of_dream()
  2. math.srand(0)
  3.     wait(300)
  4.     egroup1(-100)
  5.     wait(120)
  6.     egroup1(100)    
  7.     wait(120)
  8.     egroup2()
  9.     wait(150)
  10.     egroup1(-100)
  11.     wait(120)
  12.     egroup1(100)    
  13.     wait(120)
  14.     ebig1()
  15.     egroup2()
  16.     wait(650)
  17.     bossphase()
  18.     wait(300)
  19. end


最后的附件是code

你需要将其放在mod目录下,并修改setting.lua中的设置为mod='dream',就能够跑这个demo了~

[ 此帖被resty在2011-03-20 09:19重新编辑 ]
附件: dream.zip (4 K) 下载次数:8
发帖
82
信仰
1
蓝点
68
符卡
0
只看该作者 板凳  发表于: 2011-03-20
感谢resty大,coroutine是好东西啊,做这个很方便。不过我想到另一种思路可能代码看起来更直观一些,只用coroutine不用closure。
发帖
179
信仰
0
蓝点
171
符卡
0
只看该作者 地板  发表于: 2011-03-20
coroutine在lua5.1.4的实现里还不算完美,据说5.2加强了不少~
另外从语法角度来看,lua的closure写起来还不够漂亮,包括lambda表达式的写法,我都不很喜欢。 在没有closure的语言中,一般会使用对象来模拟一个closure,然而在没有对象的语言中,用closure来模拟也很有意思。比如我们完全可以抛弃lua的语法糖':',自己实现一套华丽丽的对象模型(虽然我一般不倾向于这么干),哪天有空再来写个玩好了。

另外这个加油做,我很看好哦~
发帖
82
信仰
1
蓝点
68
符卡
0
只看该作者 4楼 发表于: 2011-03-20
就是把对象的所有数据都存在一个closure里么?
另外
3.里面创建enemy的方法是coroutine中将闭包返给主进程执行是由于我有些问题搞不定,反正直接调用New就会悲剧= =
会不会是因为coroutine和主线程的lua_State不一样导致的?我翻了下lua的文档,lua_newthread应该就是对应coroutine.create的吧,这个函数返回了一个新的lua state。
哦,我知道为什么要用闭包了。

按理说Lua没必要把事情搞这么复杂啊,我怀疑是我哪里写的不规范,导致其不能在coroutine下工作。
[ 此帖被隔壁的桌子在2011-03-20 11:08重新编辑 ]
发帖
179
信仰
0
蓝点
171
符卡
0
只看该作者 5楼 发表于: 2011-03-20
估计是这个原因,嘛不要太在意拉~
关于对象模型么,放在闭包里是肯定的
而且这里面可以实现 private public virtual 等不同的特性哦~
发帖
82
信仰
1
蓝点
68
符卡
0
只看该作者 6楼 发表于: 2011-03-20
嗯,期待一下,不知道到时候我会不会把这玩意儿再重写一遍  
New函数的问题解决了,确实是那个原因。
[ 此帖被隔壁的桌子在2011-03-20 15:40重新编辑 ]
发帖
7
信仰
0
蓝点
20
符卡
0
只看该作者 7楼 发表于: 2011-03-22
高玩~ ,有时间研究下~
发帖
3
信仰
0
蓝点
0
符卡
0
只看该作者 8楼 发表于: 2011-07-28
好东西阿。不过没有关于难度的吗。
描述
快速回复

您目前还是游客,请 登录注册
批量上传需要先选择文件,再选择上传
认证码: