不可变的游戏对象

时间:2014-01-31 15:46:30

标签: f# immutability

我正试图围绕F#,并且在大多数情况下它是有意义的,但人们一直说每个问题都可以用不可变的结构来解决。但是游戏中的物体呢?他们有不断变化的变量(x,y,xspeed,yspeed,score,health等......)

假设我有一个玩家对象,用C#定义为:

public class Player extends GameObject
{
    private int score = 0;
    private int x = 0;
    private int y = 0;    
    ... more variables

    public Player()
    {

    }

    public void addScore(int amount)
    {
        score += amount;
    }

    public void addX(int amount)
    {
        x += amount;
    }

    public void addY(int amount)
    {
        y += amount;
    }

    ... more methods
}

这段代码的变量本身就是:“变量”。这段代码将如何转换为不可变代码?

3 个答案:

答案 0 :(得分:4)

根据定义,不可变的东西不能改变,因此为了更新不可变的游戏对象,必须创建包含更新的新对象。

type Player(score, x, y) =
    member this.addScore(amount) =
        Player(score + amount, x, y)

    member this.addX(amount) =
        Player(score, x + amount, y)

    member this.addY(amount) =
        Player(score, x, y + amount)

let first = Player(0, 0, 0)
let firstMoved = first.addX(2) 

将每个变量传递给构造函数可能会变得笨拙和缓慢。为避免这种情况,请将数据组织到不同的这将允许重用较小的对象来创建更大的对象。由于这些对象是不可变的,因此它们将始终包含相同的数据,并且可以自由地重复使用而无需担心。

type Stats(score, lives) =
    member this.addPoints(points) =
        Stats(score + points, lives)

type Location(x, y) =
    member this.move(x2, y2) =
        Location(x + x2, y + y2)

type Player(stats : Stats, location : Location) =
    member this.addScore(amount) =
        Player(stats.addPoints(amount), location)

    member this.addX(amount) =
        Player(stats, location.move(amount, 0))


let first = Player(Stats(0, 1), Location(0, 0))
let firstMoved = first.addX(2) 

F#还支持可变数据。你可以写这样的原始课。

type Player() =
    inherit GameObject()

    let mutable score = 0
    let mutable x = 0
    let mutable y = 0

    member this.addScore(amount) =
        score <- score + amount

    member this.addX(amount) =
        x <- x + amount

    member this.addY(amount) =
        y <- y + amount

答案 1 :(得分:1)

您可以使用记录为玩家建模,然后添加更新功能:

type Player = {
    score: int
    x: int
    y: int
}

let addX (player: Player) amount = { player with x = (player.x + amount) }

答案 2 :(得分:0)

正如其他人所说,您可以通过创建具有更新属性的新对象来更新不可变对象的属性。但是,让我试着说明一下如何使用某些代码。

为简单起见,假设您正在对粒子系统进行建模,其中每个粒子都会根据其速度移动。

您的模型由一组粒子组成,这些粒子会更新,然后每帧渲染到屏幕上。

在C#中,您的模型实现可能如下所示:

public class Model
{
    public class Particle
    {
        public double X { get; set; }
        public double Y { get; set; }
        public double Vx { get; set; }
        public double Vy { get; set; }
    }

    public Model()
    {
        this.particles = /* ... initialize a set of particles ... */
    }

    public void Update()
    {
        foreach (var p in this.particles)
        {
            p.X += p.Vx;
            p.Y += p.Vy;
        }
    }

    public void Render()
    {
        /* ... clear video buffer, init something, etc ... */
        foreach (var p in this.particles)
        {
            this.RenderParticle(p);
        }
    }
}

非常简单。正如您所看到的,每个帧都会遍历整个粒子集,并根据它的速度更新每个粒子的坐标。

现在,如果粒子是不可变的,就没有办法改变它的属性,但正如所说,我们只是创建一个具有更新值的新粒子。但坚持一秒,这是不是意味着我们必须用我们的集合中更新的原始粒子对象替换原始粒子对象?是的,它确实。并不意味着设置现在必须可变?看起来我们只是将可变性从一个地方转移到另一个地方......

好吧,我们可以重构每帧重建整个粒子集。但是,存储集合的属性必须是可变的......嗯。你跟着吗?

似乎在某些时候你必须失去不变性才能产生效果,并且根据 那个点,你的程序将会或不会有一些(可以说是)有用的属性

在我们的案例中(我们正在制作视频游戏),我们在技术上可以一直推迟可变性,直到我们在屏幕上绘图。屏幕是可变的很好,否则我们必须每帧创建一个新的显示器:)

现在,让我们看看如何在F#中实现这一点:

type Particle = {
    X : double
    Y : double
    Vx : double
    Vy : double
}

type Model = { Particles : Particle seq }

module Game =
    let update model =
        let updateParticle p =
            { p with X = p.X + p.Vx; Y = p.Y + p.Vy }

        { model with
            Particles = seq {
                for p in model.Particles do
                    yield updateParticle p } }

    let render model =
        for p in model.Particles do
            Graphics.drawParticle p

请注意,为了保持不变性,我们必须使Update方法成为一个函数,它接受一个模型对象并将其更新。 Render现在也将模型对象作为参数,因为我们不再将它保留在内存中,因为它是不可变的。

希望这有帮助!