3.用WPF实现2048

2019-03-29  本文已影响0人  半半百

终于来到了这里,废话不多说,开搞!

界面实现

想怎么画就怎么画,为所欲为之为所欲为。


<Window x:Class="WpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp"
        mc:Ignorable="d"
        Title="MainWindow" Height="550" Width="400" KeyDown="Window_KeyDown">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="60*"/>
            <RowDefinition Height="60*"/>
            <RowDefinition Height="400*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="2*"/>
            <ColumnDefinition Width="1*"/>
            <ColumnDefinition Width="1*"/>
        </Grid.ColumnDefinitions>
        <Viewbox Grid.RowSpan="2" >
        <Label Content="2048" />
        </Viewbox>
        <Viewbox Grid.Row="0" Grid.Column="1" VerticalAlignment="Bottom">
            <TextBlock Text="  Score  "/>
        </Viewbox>
        <Viewbox Grid.Row="1" Grid.Column="1" VerticalAlignment="Top">
            <TextBlock x:Name="Score" Text="    0    " />
        </Viewbox>
        <Viewbox Grid.Row="0" Grid.Column="2" VerticalAlignment="Bottom">
            <TextBlock Text="   Best   " />
        </Viewbox>
        <Viewbox Grid.Row="1" Grid.Column="2" VerticalAlignment="Top">
            <TextBlock x:Name="Best"  Text="    0    " />
        </Viewbox>
        <Grid x:Name="grid" Grid.Row="2" Grid.ColumnSpan="3">
        </Grid>
    </Grid>
</Window>

有个小细节,因为中间使用了 Viewbox 来显示分数,Viewbox会根据文本长度自动调节字体的大小,如下。


这里需要将字符串统一设置成固定的长度,当小于这个长度时两边补充些空格,让分数显示更统一一些。
为String增加一个扩展方法,实现这个功能。
这时为Score赋值时可以直接写为
Score.Text = g.Score.ToString().FormatForLength(5);

public static string FormatForLength(this string _string,int len)
        {
            string res = _string.ToString();
            int count = len - res.Length;
            string temp = "";
            for (int i = 0; i < count; i++)
            {
                temp += " ";
            }
            res = temp + res + temp;
            return res;
        }
动画逻辑

2048的游戏里,当玩家无操作的时候,无动画,玩家操作一次播一次动画,这种情况可以很简单的将动画部分抽离出来。

开始游戏 >>> 显示当前游戏状态 >>> 玩家操作 >>> 生成动画并播放 >>> 显示当前游戏状态 >>> 玩家操作 >>> 生成动画并播放······

那么这里可以简单的将当前游戏状态,和生成动画 抽象出来,不封装太深,整个接口吧。

    interface IWpf2048UI
    {       
        //为了将每个方块和它的动画关联起来,建立一个NameScope,
        //生成方块时将方块加进去,生成动画时从中拿出来关联
        NameScope NameScope { get; set; }
        //动画的持续时间
        Duration Duration { get; set; }
        //生成游戏画板(当前游戏状态)
        Panel PanelFactory(G2048 g);
        //生成游戏动画(生成动画并关联到画板中)
        Storyboard StoryboardFactory(Dictionary<BizLogic.Point, BizLogic.Point> moved, Size blockSize);
    }
交互逻辑

有了上面的接口,交互逻辑如下。

有个小细节,当游戏动画正在播放时,玩家又一次按下了方向键可能会导致此次动画播放有问题。
这里可以加上一把锁🔒,当开始播放动画时加锁,加锁期间不响应玩家操作,播放完毕后开锁。具体实现如下 bool operationLock

    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            init();
        }
        G2048 g;
        IWpf2048UI iWpf2048UI;
        bool operationLock = false;
        int Row;
        int Colum;
        /// <summary>
        /// 初始化
        /// </summary>
        private void init()
        {
            Row = 5;
            Colum = 5;
            g = new G2048(Row,Colum);
            this.SetValue(NameScope.NameScopeProperty, iWpf2048UI.NameScope);
            this.grid.Children.Add(iWpf2048UI.PanelFactory(g));
        }

        /// <summary>
        /// 按键事件
        /// </summary>
        private void Window_KeyDown(object sender, KeyEventArgs e)
        {

            if (operationLock)
            {
                return;
            }
            G2048.Direction direction;
            switch (e.Key)
            {
                case Key.Up: direction = G2048.Direction.Up; break;
                case Key.Down: direction = G2048.Direction.Down; break;
                case Key.Left: direction = G2048.Direction.Left; break;
                case Key.Right: direction = G2048.Direction.Right; break;
                default:
                    return;
            }
            if (!g.Operate(direction))
            {
                return;
            }
            playAnim();
        }

        /// <summary>
        /// 播放动画
        /// </summary>
        private void playAnim()
        {
            Console.WriteLine(g.ToString());
            operationLock = true;
            Storyboard storyboard = iWpf2048UI.StoryboardFactory(g.Moved, new Size(grid.ActualWidth / Row, grid.ActualHeight / Colum));
            storyboard.Completed += storyboard_Completed;
            storyboard.Begin(this);
        }

        /// <summary>
        /// 动画结束
        /// </summary>
        void storyboard_Completed(object sender, EventArgs e)
        {
            grid.Children.Clear();
            grid.Children.Add(iWpf2048UI.PanelFactory(g));
            Score.Text = g.Score.ToString().FormatForLength(5);
            operationLock = false;
        }
接口的实现

接下来实现一下动画接口IWpf2048UI,因为将生成方块和生成方块的动画分割开了,需要一个映射关系NameScope,统一将生成的方块按坐标命名,存入NameScope,生成动画时按命名附加。

这里统一下方块的命名方式,按其坐标命名,即

        public static string Sign(this BizLogic.Point point)
        {
            return "X"+point.X +"Y"+ point.Y;
        }

以下是默认的动画呈现方式Wpf2048UIDefault

 class Wpf2048UIDefault : IWpf2048UI
    {
        public NameScope NameScope { get; set; } = new NameScope();
        public Duration Duration { get; set; } = new Duration(TimeSpan.FromMilliseconds(500));

        public Panel PanelFactory(G2048 g)
        {
            Grid tempGrid = new Grid();

            for (int i = 0; i < g.Row; i++)
            {
                tempGrid.RowDefinitions.Add(new RowDefinition());
            }
            for (int j = 0; j < g.Colum; j++)
            {
                tempGrid.ColumnDefinitions.Add(new ColumnDefinition());
            }
            for (int i = 0; i < g.Row; i++)
            {
                for (int j = 0; j < g.Colum; j++)
                {
                    if (g.Map[i, j] == 0)
                    {
                        continue;
                    }
                    FrameworkElement element = this.FrameworkElementFactory(new BizLogic.Point(i, j), g.Map[i, j]);
                    element.SetValue(Grid.RowProperty, i);
                    element.SetValue(Grid.ColumnProperty, j);
                    tempGrid.Children.Add(element);
                }
            }
            return tempGrid;
        }

        private FrameworkElement FrameworkElementFactory(BizLogic.Point point,int value)
        {
            Border border = new Border();
            Transform transform = new TranslateTransform();
            border.RenderTransform = transform;         
            NameScope.Add(point.Sign(),transform);
            Viewbox viewbox = new Viewbox();
            TextBlock block = new TextBlock();
            if (value != 0)
            {
                block.Text = value.ToString().FormatForLength(3);
                block.VerticalAlignment = VerticalAlignment.Center;
                block.HorizontalAlignment = HorizontalAlignment.Center;
                block.TextAlignment = TextAlignment.Center;
            }
            border.Margin = new Thickness(3);
            viewbox.Child = block;
            border.Child = viewbox;
            border.Background = new SolidColorBrush(getColorByValue(value));
            return border;
        }

        public virtual Storyboard StoryboardFactory(Dictionary<BizLogic.Point, BizLogic.Point> Moved,Size blockSize)
        {
            if (Moved.Count == 0)
            {
                return null;
            }
            Storyboard storyboard = new Storyboard();
            DependencyProperty dp;
            double len;
            //如果是横向
            if (Moved.First().Key.X== Moved.First().Value.X)
            {
                dp = TranslateTransform.XProperty;
                len = blockSize.Width;
            }
            else
            {
                dp = TranslateTransform.YProperty;
                len= blockSize.Height;
            }
            foreach (var move in Moved)
            {
                double lenTemp = (move.Value.X - move.Key.X + move.Value.Y - move.Key.Y) * len;


                //获取一个移动动画
                DoubleAnimation daTemp = DoubleAnimationFactory(lenTemp);

                //使指定的动画的UI载体
                Storyboard.SetTargetName(daTemp, move.Key.Sign());
                //使动画与UI载体的属性相关联
                Storyboard.SetTargetProperty(daTemp, new PropertyPath(dp));
                
                //指定场景的时间,并把各个对像的动画添加到场景里面
                storyboard.Children.Add(daTemp);
            }
            storyboard.Duration = Duration;
            storyboard.Completed += storyboard_Completed;
            return storyboard;
        }

        void storyboard_Completed(object sender, EventArgs e)
        {
            NameScope.Clear();
        }
      
        /// <summary>
        /// 根据数值返回不同的显示内容
        /// </summary>
        protected virtual string getStringByValue(int value)
        {
            return value.ToString().FormatForLength(3);
        }
        /// <summary>
        /// 根据数值返回不同的颜色
        /// </summary>
        protected virtual Color getColorByValue(int value)
        {
            Color color = Colors.BurlyWood;
            switch (value)
            {
                //返回不同的颜色
            }
            return color;
        }
测试下

将接口实现放进去。
接下来就是见证奇迹的时刻,F5走起

            iWpf2048UI = new Wpf2048UIDefault
            {
                Duration = new Duration(TimeSpan.FromMilliseconds(500))
            };
上一篇下一篇

猜你喜欢

热点阅读