Silverlight 2.5D RPG游戏技巧与特效处理:(十五)任务剧情

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

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

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

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

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

ExpandedBlockStart.gif 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 {  get private   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  =   true return ; }
                    
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( this null ); }
            
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 " 0 1 , 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 " 0 1 , 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 " 1 0 , 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 " 1 0 , 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( this null ); }
            };
            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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值