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

任务是贯穿游戏剧情发展的核心线索,具有极强的多元性、组合性、循环性与随机性;它的设计原则浓缩起来便是:触发-执行-完成。别小看这短短6个字,里面的学问可大了,由什么触发、如何触发的,因素很多;怎样执行、什么样的过程,一切随便;怎么算完成,完成后的奖励是啥,什么都行。而不同的故事背景、不同的操作玩法,在任务设计方面都会大相径庭。比如RPG游戏,角色扮演即是虚拟人生,需要还原一个完整而虚幻的世界,因此它的任务系统通常会被设计得极其丰富,可以比喻:人生有多复杂,RPG的任务系统就有多庞大;又比如SLG游戏,大致以独立的场景为线索进行剧情串联,因而任务系统设计起来便相对简单得多;除此之外还有RTS、FPS等类型游戏,它们的侧重点在于策略和战斗操控,通常只需面对面的对话、走到指定地点或杀掉某个对象便可触发/完成剧情任务,被视为最简单的任务模式之一。由此可见,不同类型游戏,所强调的核心玩法及需要表达的世界观都不同,作为游戏进展的纽带,任务系统也需酌情设计。
可见,MMORPG的任务系统确实非常非常复杂,一款成功的MMORPG离不开其背后优秀的任务系统,我们需要不断借鉴与微创新,比如《任务系统设计思路》、《游戏任务多样化分析》等在分析魔兽世界的任务系统方面都具有很好的参考价值,网上还能搜罗到相当多的类似资料,作为策划的主要工作,我们暂且就不掺和了。
本节,我只想将其中一个微小却又极具传统韵味的分支:任务剧情功能实现呈现给大家;通过它,最终将游戏中的所有角色都关联了起来,作为庞大而复杂的任务系统的开端,今后若有充足时间我会陆续一一补充。
剧情任务通常是游戏的主线,尤其在单机游戏中。玩家可能不会记得为哪个工匠打了把刀,为哪个NPC杀了些怪,但是肯定会记住游戏中那些贯穿始终的儿女情长,乱世纷争。于是模仿经典的武侠网游《剑侠世界》的剧情功能设计,我们同样可以在Silverlight中制作出独具特色的剧情写实功能:
Silverlight 2.5D RPG游戏技巧与特效处理:(十五)任务剧情
以上是最终效果截图,我将剧情对话控件由上至下分成功能窗口、特写窗口和剧情窗口;通过一些渐进渐出的动画,每次与NPC对话时触发剧情都会平缓的隐藏掉UI并突出剧情界面,此类特写便是让玩家更加重视主线内容的重要手法。剧情的呈现作为单独的一个控件,其中集成了阻断向下路由的鼠标操作,同时也需要处理好游戏画面的层次感让玩家更有身临其境之感觉,即便是在战斗中,如触发了剧情,游戏世界的时间(循环)依旧运转而不会因为你的私事造成一丝滞留:
Silverlight 2.5D RPG游戏技巧与特效处理:(十五)任务剧情Silverlight 2.5D RPG游戏技巧与特效处理:(十五)任务剧情DramaDialogue /// /// 剧情对话板 /// public sealed class DramaDialogue : UIBase { /// /// 显示时触发 /// public event EventHandler Showing; /// /// 消失时触发 /// 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) }; /// /// 是否正在显示 /// public bool IsShowing { get; private set; } /// /// 获取或设置剧情播放速度 /// 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 { } } }
Silverlight 2.5D RPG游戏技巧与特效处理:(十五)任务剧情
到此,本系列Demo的编写已告一段落。有些技巧和功能太小便不作为单独章节详细讲述了,比如地图的视口缓动、地形的预测与碰撞处理、角色的特殊动画体系(如弹开、击飞、颤栗等)的处理;当然也还存在很多可拓展元素,比如《博得之门》中的游戏暂停、录像功能;格斗游戏中增强体验的烟尘、声效和印记效果等。代码方面除还可能进一步优化的小部分外,整体框架在反复的思考与重构后已近乎成熟;相比两年前的QXGameEngine,可谓翻天覆地的变化。
最后剩下几节,我将为本系列Demo焊接上游戏登陆模块,赋予它一个相对完整的商业产品流程。之前写过的两篇《动态资源》和《多国语言本地化》便是它的开头,重新整编入本系列作为章节。
本系列源码请到目录中下载
在线演示地址:http://silverfuture.cn
Tags: 

延伸阅读

最新评论

发表评论