面向对象的程序设计在游戏开发中使用(七):享受劳动果实

作者:Kingfeng 奶牛关 2019-08-30

我并不是游戏开发的从业人员,甚至连软件开发都不是,但至少我是程序员。我认为,一个【面向对象】的理念在学习过程中的重要性远大于对于代码本身含义的认识。这一点可以在诸多书籍中得到论证,但很奇怪的一件事是,绝大部分的视频入门教程和并没有过多的强调这一点。

系列收藏夹:https:/cowlevel.net/collection/3476202

1增加我们的类和修改漏洞

既然我们已经把数字独立出来了,该轮到我们享受劳动成果啦!

我们需要创造一个新的数,分数,从基础的数继承

因此要遵循本来的【协议】来实现

作为一个分数,我们同时接受分号(|)

  1. func can_push(num):
  2.         return ((num is int) or (num == '|'))
  3.         pass
复制代码

另外一个地方,我们将接受分母和分子,每次输入“分号”就把“光标”定位到另外一方

  1. func push_number(num):
  2.         if num is int:
  3.                 if pushMode :
  4.                         number[0]*=10
  5.                         number[0]+=num
  6.                 else :
  7.                         number[1]*=10
  8.                         number[1]+=num
  9.         else:
  10.                 pushMode = !pushMode
  11.         pass
复制代码

在我们显示数字的时候,我们将分母与分子使用分号隔离

  1. func get_number():
  2.         return (str(number[0])+"|"+str(number[1]))
  3.         pass
复制代码

而我们作为一个“等级”更高的数字,应该从更低的数字接受输入

  1. func upgread(num):
  2.         if num.get_class() == "AbsBaseNumber":
  3.                 number[0] = num.get_data()
  4.         pushMode = false
  5.         pass

  6. func push_number(num):
  7.         if number.can_push(num) :
  8.                 number.push_number(num)
  9.         elif AbsFracNumber.new().can_push(num):
  10.                 var tN = AbsFracNumber.new()
  11.                 tN.upgread(number)
  12.                 number = tN
  13.         pass
复制代码

最后,我们更改加减的实现

并且为分数加入一个约分的功能

  1. func redu():
  2.         var redN
  3.         redN = number[1] if number[0]>number[1] else number[0]
  4.         while(                                                                                                                \
  5.                 (number[0] %redN != 0) or (number[0] %redN != 0)                \
  6.         ):
  7.                 redN -= 1

  8.         if redN!=1 :
  9.                 number[0] /=redN
  10.                 number[1] /=redN
  11.         pass

  12. func do_add(num, ans):
  13.         var numD = num.get_data()
  14.         if num.get_class() == "AbsBaseNumber":
  15.                 ans.set_number([number[0] + numD*number[1], number[1]])
  16.                 ans.redu()
  17.         else:
  18.                 ans.set_number([        number[0]*numD[1]+number[1]*numD[0]        \
  19.                         ,                                        number[1]*numD[1]                                ])
  20.                 ans.redu()
  21.         return ans
  22.         pass
复制代码

当我们写到这里突然会发现一个问题

A-B不等于B-A。这是一个BUG,而我们要去修复他

在现有逻辑之上,当我们需要B-A的时候,我们需要为结果装上一个负号

  1. if num1.can_operation(form[0]):
  2.                 tNum = num1.operation(form[0], symb, tNum)
  3.         else:
  4.                 tNum = form[0].operation(num1, symb, tNum)

  5.                 if symb == BusiLogic.BT["SUB"]:
  6.                         tNum.do_mins()
复制代码

也因为如此,我们会把MyNumber和所有的抽象数字都增加取反的函数


Yeah,这是一个能用的计算器对吧?但是我们的代码够好吗

2堆叠代码

我们可以注意到,我们的“分式”是int/int

但是对于一个更复杂的计算器,或者一个更抽象的数字,我们期待的是一个A/B

所以对于我们的分式,我们更期待一个

[MyNumber,MyNumber]

而并不是

[int,int]

在修改之后,我们在推入数字

  1. func push_number(num):
  2.         if num is int:
  3.                 if pushMode :
  4.                         number[0].push_number(num)
  5.                 else :
  6.                         number[1].push_number(num)
  7.         else:
  8.                 pushMode = !pushMode
  9.         pass
复制代码

这样我们在引入根号或者其他什么东西之后,我们可以很简单的引入成为根号分之根号,

由于在分数运算中我们引入了乘法,而我们并没有对乘法的具体实现,所以在这个项目中并没有办法完成这种操作,但是这并不会阻挡我们的讨论

在一个更直观的例子中,我实现了一个右键的“弹出菜单”

  1. func signal_press():
  2.         if message.size() == 1:
  3.                 emit_signal("en_press",[message[0]])
  4.         else:
  5.                 next_pop_node = next_pop.instance()
  6.                 add_child(next_pop_node)
  7.                 next_pop_node.init(message[1],get_parent().rect_position + Vector2(100,20*buttonN))
  8.                 next_pop_node.connect("popup_return",self,"_sub_popup")
  9.         pass
复制代码

当按钮有子菜单,就建立一个子菜单,如果按钮没有子菜单,就返回按钮按下的信息

很简单对吧?

在这个例子里,我们可以轻易的创建出二层到多层菜单,而不用更多的代码来完成对多级菜单的维护。

3Are We Cool Yet?

回到我们的代码当中,我们的代码足够“完美”吗?

首先我们实现了一个万能数,这实际上是一个“状态类”

关于状态模式

但是我们万能数里的抽象数并不是一个抽象数类,而是像“整数”“分数”这样具体的数字

我们可以使用编程语言提供的抽象类来将所有抽象数真的实现一个”抽象数类“然后所有抽象数继承自这个类,这会是一个非常好的习惯。

虽然如此,我们实际上已经遵循了抽象类的原理,那就是所有子类都必须实现抽象类的功能,只不过这个抽象类并没有被我们写进代码里。

另外,我们实际上把计算功能放在数字内部了,这是因为我们在设计之初就把计算作为数字的功能之一,这有利于在规划不明的情况下可以更好的把一个新的抽象数加入到我们的程序中,这种偶合,在数字中看起来还行的通,但是很多时候,我们希望把“数字”和“运算”隔离开并且可以分开维护。

在加入新的抽象数之后,我们并不是简单的把数字加入到代码中就可以了,我们还需要为新的数字增加按键,为新的数字编号。

更麻烦的是,每次加入一个新的数字类型,我们需要在万能数的”升级“规则里加入相应的新类型,我们可以设立一个列表,把每个抽象数的”样板“加入到列表中,使用循环遍历这个列表直到找到适合的数字,替代我们”特立独行“的升级规则

同时,虽然我并没有展示界面生成的代码,当有了新的数字,和他专属的符号,我们需要手动添加专属的符号,然后定义他的位置

这对于一个计算器是理所当然的,但在游戏里,这明显会增加维护成本

就像Minecraft中,制作Mod并不需要在游戏里指明位置,只需要通知主程序我们需要加入的东西,主程序就可以为我们安排新的按钮和位置,那么如果我们的目标是使得一些开发者可以为我们计算器提供扩展,制作一些MOD,我们会需要一个注册机制

额外的,我们在代码中没有加入任何的损害管制,或者灾难控制。

在任何程序中,我们不应该过于乐观,而是要思考“如果不是怎么办”

这在多人开发中有助于维护多个开发者的同步性

我们在开发一个抽象数之前,我们本应该创建一个“测试”

所有的抽象数都应该进入这个“测试”来使用规则运行每个抽象数,这个测试应该尽可能的覆盖所有的情况,并且不应该覆盖所有错误的情况。只有通过了测试的抽象数才可以被纳入到代码之中

3元编程

什么是元编程?

元编程的含义核心是“用代码去制造代码”

这并不是一种编程技巧,元编程是一种更加方便的的高度自定义

方便的元编程从来不是一种编程语法糖,而是“烟草”会让你上瘾并把你的身体糟蹋的一塌糊涂

我们应该在一个项目的最初,更细节的规划好要使用的功能,并把这些功能“固化“到代码里

而不是在编程过程中把一些变化”元编程“进代码里,因为这种变化并不是简单的改变某个变量,或者某个字符串,而是改变了程序本身

这会使得程序更加难以维护

同样的,针对一个功能,使用元编程会更简单,但会打破”统一“。我们应该只在必要的情况下作为最后方案使用元编程,而不是把元编程当作我们的生存必需品。

4总结

这个系列到此完结,

大家可以访问

码云仓库

来访问我们所用到的代码,这并不是一个完整的项目,因为项目的代码实际上是很不完全的

如果有时间,未来可能会更新godot的学习历程

godot的编程体验很好,而且我相信godot在一段时间以内会发展的越来越好

因此,我希望人们更多的了解这个引擎

系列文章
面向对象的程序设计在游戏开发中使用(一):类
面向对象的程序设计在游戏开发中使用(二):方法
面向对象的程序设计在游戏开发中使用(三):三大特性
面向对象的程序设计在游戏开发中使用(四):五大原则

面向对象的程序设计在游戏开发中使用(五):基本计算器
面向对象的程序设计在游戏开发中使用(六):数与抽象数

面向对象的程序设计在游戏开发中使用(七):享受劳动果实

作者:Kingfeng
来源:奶牛关
原地址:https://cowlevel.net/article/2055693


最新评论
暂无评论
参与评论

商务合作 查看更多

编辑推荐 查看更多