posts - 156,  comments - 5252,  trackbacks - 0
公告

任务是贯穿游戏剧情发展的核心线索,具有极强的多元性、组合性、循环性与随机性;它的设计原则浓缩起来便是:触发-执行-完成。别小看这短短6个字,里面的学问可大了,由什么触发、如何触发的,因素很多;怎样执行、什么样的过程,一切随便;怎么算完成,完成后的奖励是啥,什么都行。而不同的故事背景、不同的操作玩法,在任务设计方面都会大相径庭。比如RPG游戏,角色扮演即是虚拟人生,需要还原一个完整而虚幻的世界,因此它的任务系统通常会被设计得极其丰富,可以比喻:人生有多复杂,RPG的任务系统就有多庞大;又比如SLG游戏,大致以独立的场景为线索进行剧情串联,因而任务系统设计起来便相对简单得多;除此之外还有RTSFPS等类型游戏,它们的侧重点在于策略和战斗操控,通常只需面对面的对话、走到指定地点或杀掉某个对象便可触发/完成剧情任务,被视为最简单的任务模式之一。由此可见,不同类型游戏,所强调的核心玩法及需要表达的世界观都不同,作为游戏进展的纽带,任务系统也需酌情设计。

因此,MMORPG的任务系统确实非常非常复杂,一款成功的MMORPG离不开其背后优秀的任务系统,我们需要不断借鉴与微创新,比如《任务系统设计思路》、《游戏任务多样化分析》等在分析魔兽世界的任务系统方面都具有很好的参考价值,网上还能搜罗到相当多的类似资料,作为策划的主要工作,我们暂且就不掺和了。

本节,我只想将其中一个微小却又极具传统韵味的分支:任务剧情功能实现呈现给大家;通过它,最终将游戏中的所有角色都关联了起来,作为庞大而复杂的任务系统的开端,今后若有充足时间我会陆续一一补充。

剧情任务通常是游戏的主线,尤其在单机游戏中。玩家可能不会记得为哪个工匠打了把刀,为哪个NPC杀了些怪,但是肯定会记住游戏中那些贯穿始终的儿女情长,乱世纷争。于是模仿经典的武侠网游《剑侠世界》的剧情功能设计,我们同样可以在Silverlight中制作出独具特色的剧情写实功能:

以上是最终效果截图,我将剧情对话控件由上至下分成功能窗口、特写窗口和剧情窗口;通过一些渐进渐出的动画,每次与NPC对话时触发剧情都会平缓的隐藏掉UI并突出剧情界面,此类特写便是让玩家更加重视主线内容的重要手法。剧情的呈现作为单独的一个控件,其中集成了阻断向下路由的鼠标操作,同时也需要处理好游戏画面的层次感让玩家更有身临其境之感觉,即便是在战斗中,如触发了剧情,游戏世界的时间(循环)依旧运转而不会因为你的私事造成一丝滞留:

DramaDialogue
    /// <summary>
    
/// 剧情对话板
    
/// </summary>
    public sealed class DramaDialogue : UIBase {

        
/// <summary>
        
/// 显示时触发
        
/// </summary>
        public event EventHandler Showing;

        
/// <summary>
        
/// 消失时触发
        
/// </summary>
        public event EventHandler Hiding;

        Rectangle topRect 
= new Rectangle() { Fill = new ImageBrush() { ImageSource = GlobalMethod.GetImage("UI/DramaLine.jpg", UriType.Project), Stretch = Stretch.Fill } };
        Rectangle middleRect 
= new Rectangle() { Fill = new ImageBrush() { ImageSource = GlobalMethod.GetImage("UI/GrayRect.png", UriType.Project), Stretch = Stretch.Fill } };
        Rectangle bottomRect 
= new Rectangle() { Fill = new ImageBrush() { ImageSource = GlobalMethod.GetImage("UI/DramaLine.jpg", UriType.Project), Stretch = Stretch.Fill } };
        Canvas topCanvas 
= new Canvas();
        Canvas bottomCanvas 
= new Canvas();
        EntityObject closer 
= new EntityObject() { Source = GlobalMethod.GetImage("UI/EndDialogue.png", UriType.Project) };
        TextBlock tip 
= new TextBlock() { Text = "【按Esc退出对话】", Foreground = new SolidColorBrush(Colors.Orange), FontSize = 18, TextWrapping = TextWrapping.Wrap };
        TextBlock content 
= new TextBlock() { Foreground = new SolidColorBrush(Colors.White), FontSize = 24, TextWrapping = TextWrapping.Wrap };
        EntityObject avatar 
= new EntityObject() { Source = GlobalMethod.GetImage("UI/Avatar0.png", UriType.Project) };
        DispatcherTimer timer 
= new DispatcherTimer() { Interval = TimeSpan.FromMilliseconds(30) };

        
/// <summary>
        
/// 是否正在显示
        
/// </summary>
        public bool IsShowing { getprivate set; }

        
/// <summary>
        
/// 获取或设置剧情播放速度
        
/// </summary>
        public int DialogSpeed {
            
get { return timer.Interval.Milliseconds; }
            
set { timer.Interval = TimeSpan.FromMilliseconds(value); }
        }

        
public DramaDialogue() {
            
this.Children.Add(topCanvas);
            topCanvas.Children.Add(topRect);
            topCanvas.Children.Add(closer); Canvas.SetTop(closer, 
10);
            
this.Children.Add(middleRect);
            
this.Children.Add(bottomCanvas);
            bottomCanvas.Children.Add(bottomRect);
            bottomCanvas.Children.Add(tip); Canvas.SetLeft(tip, 
15); Canvas.SetTop(tip, 65);
            bottomCanvas.Children.Add(content); Canvas.SetTop(content, 
10);
            bottomCanvas.Children.Add(avatar); Canvas.SetLeft(avatar, 
10);
            timer.Tick 
+= delegate {
                countText
++;
                
if (countText > drama.Content[countDrama].Length) {
                    
if (timer.IsEnabled) {
                        timer.Stop();
                        countDrama
++;
                    }
                } 
else {
                    content.Text 
= drama.Content[countDrama].Substring(0, countText);
                }
            };
            
this.MouseLeftButtonDown += (s, e) => {
                
if (IsShowing) {
                    Point p 
= e.GetPosition(closer);
                    
if (p.X >= 0 && p.X <= closer.RealWidth && p.Y >= 0 && p.Y <= closer.RealHeight) { Hide(); e.Handled = truereturn; }
                    
if (timer.IsEnabled) {
                        content.Text 
= drama.Content[countDrama];
                        timer.Stop();
                        countDrama
++;
                    } 
else {
                        
if (countDrama == drama.Content.Count) {
                            Hide();
                        } 
else {
                            countText 
= 0;
                            avatar.Source 
= GlobalMethod.GetImage(string.Format("UI/Avatar{0}.png", drama.Avatar[countDrama]), UriType.Project);
                            timer.Start();
                        }
                    }
                }
                e.Handled 
= true;
            };
        }

        Drama drama;
        
int countText = 0;
        
int countDrama = 0;
        
public void Show(Drama drama) {
            IsShowing 
= true;
            
if (Showing != null) { Showing(thisnull); }
            
this.drama = drama;
            avatar.Source 
= GlobalMethod.GetImage(string.Format("UI/Avatar{0}.png", drama.Avatar[countDrama]), UriType.Project);
            Storyboard storyboard 
= new Storyboard();
            Duration duration 
= TimeSpan.FromMilliseconds(500);
            PowerEase powerEase 
= new PowerEase() { EasingMode = EasingMode.EaseOut };
            storyboard.Children.Add(GlobalMethod.CreateDoubleAnimation(topCanvas, 
"(Canvas.Top)"-topRect.Height, 0, duration, powerEase));
            storyboard.Children.Add(GlobalMethod.CreateDoubleAnimation(middleRect, 
"Opacity"01, duration, powerEase));
            storyboard.Children.Add(GlobalMethod.CreateDoubleAnimation(bottomCanvas, 
"(Canvas.Top)", Application.Current.Host.Content.ActualHeight, Application.Current.Host.Content.ActualHeight * 4 / 5, duration, powerEase));
            storyboard.Children.Add(GlobalMethod.CreateDoubleAnimation(avatar, 
"Opacity"01, duration, powerEase));
            EventHandler handler 
= null;
            storyboard.Completed 
+= handler = delegate {
                storyboard.Completed 
-= handler;
                timer.Start();
            };
            storyboard.Begin();
        }

       
public void Hide() {
            IsShowing 
= false;
            Storyboard storyboard 
= new Storyboard();
            Duration duration 
= TimeSpan.FromMilliseconds(500);
            PowerEase powerEase 
= new PowerEase() { EasingMode = EasingMode.EaseOut };
            storyboard.Children.Add(GlobalMethod.CreateDoubleAnimation(topCanvas, 
"(Canvas.Top)"0-topRect.Height, duration, powerEase));
            storyboard.Children.Add(GlobalMethod.CreateDoubleAnimation(middleRect, 
"Opacity"10, duration, powerEase));
            storyboard.Children.Add(GlobalMethod.CreateDoubleAnimation(bottomCanvas, 
"(Canvas.Top)", Application.Current.Host.Content.ActualHeight * 4 / 5, Application.Current.Host.Content.ActualHeight, duration, powerEase));
            storyboard.Children.Add(GlobalMethod.CreateDoubleAnimation(avatar, 
"Opacity"10, duration, powerEase));
            EventHandler handler 
= null;
            storyboard.Completed 
+= handler = delegate {
                storyboard.Completed 
-= handler;
                countText 
= 0;
                countDrama 
= 0;
                content.Text 
= string.Empty;
                timer.Stop();
                
if (Hiding != null) { Hiding(thisnull); }
            };
            storyboard.Begin();
        }

        
public override void AdaptiveWindowSize() {
            
try {
                topRect.Width 
= middleRect.Width = bottomRect.Width = Application.Current.Host.Content.ActualWidth;
                topRect.Height 
= Application.Current.Host.Content.ActualHeight / 14;
                middleRect.Height 
= Application.Current.Host.Content.ActualHeight;
                bottomRect.Height 
= Application.Current.Host.Content.ActualHeight * 1 / 5;
                tip.Width 
= Application.Current.Host.Content.ActualWidth * 1 / 5 - 15;
                content.Width 
= Application.Current.Host.Content.ActualWidth * 4 / 5 - 15;
                Canvas.SetLeft(closer, Application.Current.Host.Content.ActualWidth 
- closer.RealWidth -15);
                Canvas.SetLeft(content, Application.Current.Host.Content.ActualWidth 
* 1 / 5);
                Canvas.SetTop(avatar, 
-avatar.RealHeight);
                Canvas.SetTop(bottomCanvas, Application.Current.Host.Content.ActualHeight 
* 4 / 5);
            } 
catch { }
        }

    }

到此,本系列Demo的编写已告一段落。有些技巧和功能太小不便作为单独章节详细讲述了,比如地图的视口缓动、地形的预测与碰撞处理、角色的特殊动画体系(如弹开、击飞、颤栗等)处理等;当然也还存在很多可拓展元素,比如《博得之门》中的游戏暂停、录像功能;格斗游戏中增强体验的烟尘、声效和印记效果等。代码方面除还可能进一步优化的小部分外,整体框架在反复的思考与重构后已近乎成熟;相比两年前的QXGameEngine,可谓翻天覆地的变化。

最后剩下几节,我将为本系列Demo焊接上游戏登陆模块,赋予它一个相对完整的商业产品流程。之前写过的两篇《动态资源》和《多国语言本地化》便是其开头,重新整编入本系列作为章节。

本系列源码请到目录中下载

在线演示地址:http://cangod.com

posted on 2011-05-12 16:45 深蓝色右手 阅读(...) 评论(...) 编辑 收藏