将命令式Java转换为函数式Java(游戏)



我正在学习更多关于Java 8及其功能的知识,我想用它做更多的练习。例如,我有以下命令代码,用于在屏幕边界周围包装一个圆圈:

if (circle.getPosition().getX() > width + circle.getRadius()){
    circle.getPosition().setX(-circle.getRadius());
}else if (circle.getPosition().getX() < -circle.getRadius()){
    circle.getPosition().setX(width + circle.getRadius());
}
if (circle.getPosition().getY() > height + circle.getRadius()){
    circle.getPosition().setY(-circle.getRadius());
}else if (circle.getPosition().getY() < -circle.getRadius()){
    circle.getPosition().setY(height + circle.getRadius());
}
  1. 我怎么能去尝试"功能化"它?也许是一些伪代码?在我看来,可变性和状态在这个例子中似乎是固有的。函数式编程不适合游戏开发吗?我喜欢这两个,所以我想把它们结合起来。

在这个例子中,对可变性的需求并没有什么内在的东西。命令式方法是通过应用改变现有圆圈状态的副作用来修改现有圆圈。

函数式方法是有一个不可变的数据结构,然后创建一个从第一个结构中获取数据并创建新结构的函数。在您的示例中,函数式方法将使圆是不可变的,即没有setX()或setty()方法。

private Circle wrapCircleAroundBounds(Circle circle, double width, double height) {
    double newx = (circle.getPosition().getX() > width + circle.getRadius()) ? -circle.getRadius() : width + circle.getRadius()
    double newy = (circle.getPosition().getY() > height + circle.getRadius()) ? -circle.getRadius() : height + circle.getRadius()
    return new Circle(newx, newy)
}

使用Java8的功能特性,您可以想象将圆圈列表映射到换行的圆圈:

circles.stream().map(circ -> wrapCircleAroundBounds(circ, width, height))

命令式方法和函数式方法有不同的优点,例如,函数式方法本质上是线程安全的,因为它具有不可变性,所以你应该能够更容易地并行化这类代码。例如,可以同样安全地写:

circles.parallelStream().map(circ -> wrapCircleAroundBounds(circ, width, height))

我不认为函数式编程一定不适合游戏开发,但是,虽然它已经完成了,但它肯定不是一种标准方法,所以如果你使用函数式语言,你将无法获得相同级别的库支持。

正如dfeer在他的回答中所说,Java的函数式特性非常原始——你不支持代数数据类型、模式匹配等,这将使用函数式风格表达问题变得容易得多(至少在你习惯了这些习惯之后)。我同意至少阅读一点关于Haskell的知识,它有一个很好的教程:http://learnyouahaskell.com/chapters将是一个很好的入门方法。与Scala不同的是,Scala是一种多范式语言,在学习这种新风格时,您没有OOP特性可以依赖。

对于您的第一点:您通过思考代码应该实现什么来"功能化"您的示例。这是,你有一个圆,想要根据某些条件计算另一个圆。但是由于某种原因,你的命令式教育让你假设输入圈和输出圈应该存储在相同的内存位置!

要实现功能,首先要忘记内存位置并接受值。考虑每种类型就像考虑intjava.lang.Integer或其他数字类型一样。

例如,假设某个新手向您展示了如下代码:
double x = 3.765;
sin(x);
System.out.println("The square root of x is " + x);

并抱怨sin似乎不起作用。你会怎么想?

现在考虑这个:

Circle myCircle = ....;
wrapAroundBoundsOfScreen(myCircle);
System.out.println("The wrapped-around circle is now " + myCircle);
当后一种代码对你来说和前一种代码一样荒谬时,你就已经爬上了函数式编程的第一步。是的,这并不意味着不要使用您正在使用的命令式语言的某些特性,或者非常谨慎地使用它们。

这里没有太多的'功能化'适用。但至少我们可以和mutability战斗。首先是pure functions。这将有助于分离logic。让它清晰且易于测试。回答这个问题:你的代码是做什么的?它接受一些参数并返回两个参数new xy。下面的示例将使用pseudo scala编写。

因此,您需要一个函数,该函数将为x和y计算调用两次。

def (xOrY: Int, widthOrHeight: Int, radius: Int): Int = {
if (x > widthOrHeight + radius) -1*radius else  widthOrHeight + radius
// do your calculation here - return x or y values.
}

p。S>到目前为止,无论你想在哪里应用函数式风格:当你需要做一些业务逻辑时,使用函数式方法是很好的。

但不要试图使它过于复杂,因为它没有帮助。所以我不会在这个示例中做的是接下来(接下来是伪scala):

def tryToMakeMove(conditions: => Boolean, move: => Unit) = if (conditions) move()
/// DO NOT DO IT AT HOME :)
tryToMakeMove(circle.getPosition().getX() > width + circle.getRadius(),  circle.getPosition().setX(-circle.getRadius())).andThen()
   tryToMakeMove(circle.getPosition().getX() < -circle.getRadius()), circle.getPosition().setX(width + circle.getRadius()))
).andThen ... so on.

函数式程序的样子。我已经创建了高阶函数(它接受其他函数作为参数并在内部调用它)。有了这个函数,我调用了你必须做的一个操作…但是这种功能风格并没有真正的帮助。在所有。

您可以用几乎任何编程语言编写函数式代码,但是您无法轻松地学习用任何语言编写函数式编程。尤其是Java,让函数式编程变得非常痛苦,以至于那些想在JVM中进行函数式编程的人想出了Clojure和Scalaz。如果您想学习函数式的思维方式(它自然地处理哪些问题,如何处理,哪些问题比较棘手,以及它如何处理这些问题,等等),我强烈建议您花一些时间学习函数式或大部分函数式语言。基于语言质量、函数习惯用法的易用性、学习资源和社区的综合考虑,我的首选是Haskell,其次是Racket。其他人当然会有不同的看法。

我怎样才能把它"功能化"呢?也许一些伪代码?在我看来,可变性和状态似乎是内在的这个例子。

您可以尝试将可变性限制在几个函数中,并在函数中使用final变量(这迫使您使用表达式而不是语句)。这里有一个可能的方法:

Position wrapCircle(Circle circle, int width, int height) {
  final int radius = circle.getRadius();
  final Position pos = circle.getPosition();
  final int oldX = pos.getX();
  final int x = (oldX > width + radius) ? -radius : (
        (oldX < -radius) ? (width + radius) : oldX);
  final int y = // similar
  return new Position(x, y);
}
circle.setPosition(wrapCircle(circle, width, height));
除此之外,我将使wrapCircle成为Circle类的方法,以获得:
circle.wrapCircle(width, height);

或者我可以更进一步定义一个getWrappedCircle方法,它返回一个新的circle实例:

Circle getWrappedCircle(width, height) {
  newCircle = this.clone();
  newCircle.wrapCircle(width, height);
  return newCircle();
}

. .这取决于你打算如何构建剩下的代码。

提示:在Java中尽可能多地使用final关键字。它会自动形成一个更实用的风格。

函数式编程不适合游戏开发吗?我喜欢这两者,所以我想把它们结合起来。

纯函数式编程速度较慢,因为它需要大量复制/克隆数据。如果性能很重要,那么您绝对可以尝试混合方法,如上所示。

我建议尽可能多地使用不可变性,然后进行基准测试,然后仅在性能关键部分转换为可变性。

函数式编程适合游戏开发(为什么不呢?)这个问题通常更多的是关于性能和内存消耗,或者任何功能性游戏引擎是否能够在这些指标上击败现有的非功能性游戏引擎。你并不是唯一一个喜欢函数式编程和游戏开发的人。看来约翰·卡马克也是这样,看看他在2013年地震经济大会上的主题演讲,从02:05开始。他在这里和这里的笔记甚至提供了如何构建功能性游戏引擎的见解。

撇开理论基础不谈,函数式编程通常有两个概念,一个是新手,另一个是从实践的角度来看。它们是数据不变性状态缺失。前者意味着数据永远不变,后者意味着每项任务都是在没有先验知识的情况下像第一次一样执行。考虑到这一点,命令式代码有两个问题:设置器改变了圆圈的位置,代码依赖于widthheight的外部值(全局状态)。为了修复它们,让函数在每次更新时返回一个新的圆,并将屏幕分辨率作为参数。让我们应用视频中的第一条线索,并将对世界静态快照的引用和对正在"更新"的实体的引用(这里简称为this)传递给更新函数:

class Circle extends ImmutableEntity {
        private int radius;
        public Circle(State state, Position position, int radius) {
            super(state, position);
            this.radius = radius;
        }
        public int getRadius() {
            return radius;
        }
        @Override
        public ImmutableEntity update(World world) {
            int updatedX = getPosition().getX();
            if (getPosition().getX() > world.getWidth() + radius){
                updatedX = -radius;
            } else if (getPosition().getX() < -radius){
                updatedX = world.getWidth() + radius;
            }
            int updatedY = getPosition().getX();
            if (getPosition().getY() > world.getHeight() + radius){
                updatedY = -radius;
            } else if (getPosition().getY() < -radius) {
                updatedY = world.getHeight() + radius;
            }
            return new Circle(getState(), new Position(updatedX, updatedY), radius);
        }
    }
    class Position {
        private int x;
        private int y;
       //here can be other quantities like speed, velocity etc.
        public Position(int x, int y) {
            this.x = x;
            this.y = y;
        }
        public int getX() {
            return x;
        }
        public int getY() {
            return y;
        }
    }
    class State { /*...*/ }
    abstract class ImmutableEntity {
        private State state;
        private Position position;
        public ImmutableEntity(State state, Position position) {
            this.state = state;
            this.position = position;
        }
        public State getState() {
            return state;
        }
        public Position getPosition() {
            return position;
        }
        public abstract ImmutableEntity update(World world);
    }
    class World {
        private int width;
        private int height;
        public World(int width, int height) {
            this.width = width;
            this.height = height;
        }
        public int getWidth() {
            return width;
        }
        public int getHeight() {
            return height;
        }
    }

现在棘手的部分是如何影响世界和其他实体的状态。你可以遵循视频中的第二条线索,使用事件传递机制来传递这些变化,这样游戏的其余部分就会知道所有的效果。

显然,即使在改变圆的位置时,您也可以只保留事件并完全依赖它们。所以,如果你在你的实体中引入一个id,你将能够通过MoveEntity(id, newPosition) .

好,现在是我们所有人都了解Java 8的新功能特性的时候了。"功能化"某物真的不是一个有效的目标。

然而,这里的原始代码有一个很好的面向对象问题:

当你说circle. getposition (). setx(…)时,你是在干扰圆的内部状态(它的位置),而不涉及对象本身。这就打破了封装。如果circle类设计正确,那么getPosition()方法将返回位置的副本或不可变位置,因此您不能这样做。

这个是你真正需要用这段代码解决的问题…

那么,你应该怎么做呢?

好吧,你当然可以在Circle中想出一些功能接口,但老实说,如果你只有Circle,你的代码会更容易读。移动(双x,双y);

相关内容

  • 没有找到相关文章

最新更新