posts - 156,  comments - 5252,  trackbacks - 0

引言

征服玩家的不仅仅是创意,无比动人的视觉体验譬如精美的界面UI同样能让人倾慕,辅以优柔的旋律仿若一缕思绪让您身临其境而流连。深刻的第一印象无限大的冲击着玩家那份内敛的狂热,优秀的游戏作品价值将在欢呼声中被最大化激活。

9.1创建自适应布局之HUD (交叉参考:一切起源于这个真实的世界 制作精美的Mini地图① 制作精美的Mini地图② 制作游戏主菜单面板及鼠标左右键快捷技能栏)

Silverlight网页游戏中一切看得见的对象我们都可称之为UI,如精灵、魔法、地图、图标等等。除此之外作为玩家,我们时常需要以“上帝”的名义通过一些按钮去影响游戏中的对象,并获取相应反馈。于是乎UI的另一位伟大成员诞生了,它就是HUD。每款游戏都拥有它独自的一套HUD,本节Demo中我将借用《十年一剑》的相关素材,大致包括主角信息、对象信息、雷达地图、聊天窗口、工具菜单等;它们各自布局于游戏窗口顶层的边缘,并以聊天中提交主角说话内容为例向大家讲解如何实现HUD与游戏中其他对象的交互。

首先我们需要在控件项目中针对HUD的每个部分创建相应的面板类:

它们均继承之Canvas,由于都以UI描述性语句为主,代码大同小异,因此这里我选择以ChatWindow为例向大家进一步作讲解:

   /// <summary>
    
/// 聊天窗口面板
    
/// </summary>
    public sealed class ChatWindow : Canvas {

        
/// <summary>
        
/// 获取或设置X、Y坐标
        
/// </summary>
        public Point Coordinate {
            
get { return new Point(Canvas.GetLeft(this), Canvas.GetTop(this)); }
            
set { Canvas.SetLeft(this, value.X); Canvas.SetTop(this, value.Y); }
        }

        
/// <summary>
        
/// 获取或设置Z层次深度
        
/// </summary>
        public int Z {
            
get { return Canvas.GetZIndex(this); }
            
set { Canvas.SetZIndex(this, value); }
        }

        /// <summary>
        
/// 发送说话内容
        
/// </summary>
        public event EventHandler<SendEventArgs> Send;

        
/// <summary>
        
/// 说话参数
        
/// </summary>
        public sealed class SendEventArgs : EventArgs {
            
public string Content { getset; }
        }

        TextBox textBox = new TextBox() {
            Text = "http://Silverfuture.cn 深蓝色右手",
            Width = 185,
            AcceptsReturn = false,
            Foreground = new SolidColorBrush(Colors.White),
            Background = new SolidColorBrush(Colors.Transparent)
        };
        Image image = new Image() { Source = Global.GetProjectImage("HUD/3.png") };
        
public ChatWindow() {
            
this.Width = 307;
            
this.Height = 243;
            
this.Background = new ImageBrush() { ImageSource = Global.GetProjectImage("HUD/4.png") };
            
this.Children.Add(textBox); Canvas.SetLeft(textBox, 78); Canvas.SetTop(textBox, 212);
            textBox.KeyDown 
+= (s, e) => {
                
if (e.Key == Key.Enter) {
                    
if (Send != null) { Send(thisnew SendEventArgs() { Content = textBox.Text.Trim() }); }
                    textBox.Text 
= string.Empty;
                    e.Handled 
= true;
                }
            };
            
this.Children.Add(image); Canvas.SetLeft(image, 272); Canvas.SetTop(image, 208);
            image.MouseLeftButtonDown 
+= (s, e) => {
                
if (Send != null) { Send(thisnew SendEventArgs() { Content = textBox.Text.Trim() }); }
                textBox.Text 
= string.Empty;
                e.Handled 
= true;
            };
        }

    }

这是一个仅仅包含背景的聊天窗口,为了实现它与游戏主角之间的交互,我在其中还特意放置了一个文本框及图形按钮,当按钮点击时触发Send事件:

然后在MainPage中为该聊天面板实例注册Send事件:

            chatWindow.Send += (s, e) => {
                hero.Say(e.Content);
            };

    主角将执行说话行为(Say)

        /// <summary>
        
/// 说话
        
/// </summary>
        public void Say(string content) {
            
if (content.Equals("")) { return; }
            Dialog dialog 
= new Dialog(5) {
                HostWidth 
= BodyWidth,
                TopOffset 
= 30
            };
            
this.Children.Add(dialog);
            dialog.Completed 
+= new EventHandler(dialog_Completed);
            dialog.Show(content);
        }

        
void dialog_Completed(object sender, EventArgs e) {
            Dialog dialog 
= sender as Dialog;
            dialog.Completed 
-= dialog_Completed;
            
this.Children.Remove(dialog);
        }

其中Dialog是本节中我新建的RPG游戏中标准的说话小窗口控件类,通过为其内置一个DispatcherTimer实现该聊天小窗口定时消失:

代码
    /// <summary>
    
/// 说话内容框
    
/// </summary>
    public sealed class Dialog : Canvas {

        
#region 属性

        
/// <summary>
        
/// 设置属寄主宽
        
/// </summary>
        public double HostWidth {
            
set { Canvas.SetLeft(this, (value - width) / 2); }
        }

        
/// <summary>
        
/// 获取或设置具体头部偏移量
        
/// </summary>
        public int TopOffset { getset; }

        
#endregion

        
#region 构造

        
const int width = 140;
        Rectangle back 
= new Rectangle() {
            Width 
= width,
            Fill 
= new SolidColorBrush(Colors.Black),
            Stroke 
= new SolidColorBrush(Colors.Gray),
            StrokeThickness 
= 2,
            RadiusX 
= 7,
            RadiusY 
= 7,
            Opacity 
= 0.4
        };
        TextBlock content 
= new TextBlock() {
            Width 
= width - 10,
            Foreground 
= new SolidColorBrush(Colors.White),
            TextWrapping 
= TextWrapping.Wrap
        };

        DispatcherTimer dispatcherTimer 
= new DispatcherTimer();
        
/// <param name="duration">持续时间</param>
        public Dialog(int duration) {
            
this.Children.Add(back);
            
this.Children.Add(content);
            Canvas.SetLeft(content, 
5); Canvas.SetTop(content, 5);
            dispatcherTimer.Interval 
= TimeSpan.FromSeconds(duration); 
            dispatcherTimer.Tick 
+= new EventHandler(dispatcherTimer_Tick);
        }

        
void dispatcherTimer_Tick(object sender, EventArgs e) {
            Hide();
            
if (Completed != null) { Completed(thisnew EventArgs()); }
        }

        
#endregion

        
#region 事件

        
/// <summary>
        
/// 说话内容显示后
        
/// </summary>
        public event EventHandler Completed;

        
#endregion

        
#region 方法

        
/// <summary>
        
/// 显示说话框
        
/// </summary>
        
/// <param name="value">内容</param>
        public void Show(string value) {
            content.Text 
= value;
            back.Height 
= content.ActualHeight + 10;
            Canvas.SetTop(
this-back.Height + TopOffset);//减去精灵名字高度
            dispatcherTimer.Start();
        }

        
/// <summary>
        
/// 隐藏说话框
        
/// </summary>
        public void Hide() {
            content.Text 
= string.Empty;
            dispatcherTimer.Stop();
            dispatcherTimer.Tick 
-= dispatcherTimer_Tick;
        }

        
#endregion
    }

概括来说,委托和事件是C#中解耦最强有力的工具,通过它我们实现了HUD与其他游戏对象之间的双向交互。

另外,游戏中的HUD各部分布局必须随着Silverlight游戏窗口尺寸的改变而改变,因此我们需要在游戏窗口尺寸改变事件方法中添加对这些部分的自适应布局方案:

        /// <summary>
        
/// 游戏窗口尺寸改变
        
/// </summary>
        void Content_Resized(object sender, EventArgs e) {
            hero_CoordinateChanged(hero, 
new DependencyPropertyChangedEventArgs());
            
if (transition.Visibility == Visibility.Visible) { transition.AdaptToWindowSize(); }
            
//HUD各部件自适应窗体尺寸
            Canvas.SetLeft(targetInfo, Application.Current.Host.Content.ActualWidth / 2 - targetInfo.ActualWidth / 2); Canvas.SetTop(targetInfo, 0);
            Canvas.SetLeft(radarMap, Application.Current.Host.Content.ActualWidth 
- radarMap.ActualWidth); Canvas.SetTop(radarMap, 0);
            Canvas.SetLeft(menuBar, Application.Current.Host.Content.ActualWidth 
- menuBar.ActualWidth); Canvas.SetTop(menuBar, Application.Current.Host.Content.ActualHeight - menuBar.ActualHeight);
            Canvas.SetTop(chatWindow, Application.Current.Host.Content.ActualHeight 
- chatWindow.ActualHeight);
        }

    HUD各部分自己的宽、高以及游戏窗体的宽、高为依据进行边缘布局;如此,我们不论是拉伸窗体、最大化窗体、全屏还是OOB模式时,HUD将永远能保持在相对正确的位置上。

除此之外,如果游戏窗口尺寸小到一定程度后HUD中各面板会出现重叠,如果游戏中不允许出现这类现象解决方案大致有两种:

1)将HUD中的所有面板均设定成可拖动窗体;

2)在游戏窗口尺寸改变事件中检测新尺寸是否为最低限制,如果超出的取消新尺寸改变。

最后,我为游戏额外添加一个按钮用作“全屏/窗口”模式的切换以进一步测试HUD的自适应性:

    

代码
            button0.Click += (s, e) => {
                
if (Application.Current.Host.Content.IsFullScreen) {
                    Application.Current.Host.Content.IsFullScreen 
= false;
                } 
else {
                    Application.Current.Host.Content.IsFullScreen 
= true;
                }
                Content_Resized(
nullnull);
            };

9.2场景之背景音乐

游戏的趣味性将伴随着恢弘磅礴的背景音乐无限延伸,Silverlight中自带的MediaElement控件已无法满足我们对游戏音乐的多方面操控,因而需要对其重新进行了封装,取名为MusicPlayer

    /// <summary>
    
/// 媒体播放器控件
    
/// </summary>
    public sealed class MediaPlayer : Canvas {

        
#region 构造

        MediaElement media 
= new MediaElement() {
            IsHitTestVisible 
= false,
            Visibility 
= Visibility.Collapsed,
            AutoPlay 
= true,
        };

        
public MediaPlayer() {
            
this.Children.Add(media);
        }

        
#endregion

        
#region 属性

        
/// <summary>
        
/// 获取或设置音量
        
/// </summary>
        public double Volume {
            
get { return media.Volume; }
            
set { media.Volume = value; }
        }

        
#endregion

        
#region 方法

        
/// <summary>
        
/// 播放媒体
        
/// </summary>
        
/// <param name="uri">路径</param>
        
/// <param name="loop">是否循环</param>
        public void Play(string uri, bool loop) {
            media.Source 
= new Uri(Global.WebPath(string.Format("Media/{0}.mp3", uri)), UriKind.Relative);
            media.Position 
= TimeSpan.Zero;
            Start(loop);
        }

        
/// <summary>
        
/// 媒体开始
        
/// </summary>
        public void Start(bool loop) {
            media.MediaEnded 
-= media_MediaEnded;
            
if (loop) { media.MediaEnded += media_MediaEnded; }
            media.Play();
        }

        
/// <summary>
        
/// 媒体停止
        
/// </summary>
        public void Stop() {
            media.MediaEnded 
-= media_MediaEnded;
            media.Stop();
        }

        
void media_MediaEnded(object sender, RoutedEventArgs e) {
            MediaElement media 
= sender as MediaElement;
            media.Position 
= TimeSpan.Zero;
            media.Play();
        }

        
#endregion

    }

该播放器是一个简单实现,可以实现循环,同时使用起来也很简单:

music.Play(Media, true);

以场景为单位,通过在它们的配置文件中添加参数完全可以实现独立于场景的不同背景音乐,肆意张扬个性的同时大可放心,这些媒体文件都是以数据流的形式逐步获取,无须任何等待及预加载。强大的Silverlight框架为我们铺垫了一切。

本课小结:游戏界面与游戏音乐相辅相成,能否运筹帷幄关系到一款游戏产品的成与败毫不言过。十数年的经验告诉我们倾注设计者灵魂的游戏产品都将成就伟大史诗,选择进步亦或倒退??期待让世界热血沸腾那一刻的降临!

本课源码点击进入目录下载

梦想起航:届Silverlight游戏开发者论坛

参考资料:中游在线[WOWO世界] Silverlight C# 游戏开发:游戏开发技术

教程Demo在线演示地址http://silverfuture.cn

posted on 2010-10-19 11:54 深蓝色右手 阅读(...) 评论(...) 编辑 收藏