SMAPI Mod制作思路
生活随笔
收集整理的這篇文章主要介紹了
SMAPI Mod制作思路
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
StardewValley對于SMAPI下Mod制作思路
- 前言
- 一、全局事件
- 1.日志打印
- 2.I18n
- 二、繪制操作
- 1.物品單價提示
- 總結
前言
在這里就不一一介紹SMAPI的安裝使用以及簡單的mod項目搭建了,現在直接開始針對于N網優秀的mod源碼進行解析,會長期慢慢更新內容
一、全局事件
1.日志打印
在Mod入口類中 Entry 方法加入全局日志函數
public override void Entry(IModHelper helper) {// 設置全局日志函數Utils.InitLog(this.Monitor);// 初始化I18n多語言文本I18n.Init(helper.Translation);... }方法中調用,打印信息至SMAPI控制臺
string text = 'X'; // 打印內容 Utils.DebugLog($"Info {text}.", LogLevel.Info);下面是日志工具類,一個打印內容的方法,一個初始化方法
internal class Utils {/*********** Properties*********/private static IMonitor MonitorRef;/*********** Public methods*********/public static void InitLog(IMonitor monitor){Utils.MonitorRef = monitor;}public static void DebugLog(string message, LogLevel level = LogLevel.Trace){ #if WITH_LOGGINGDebug.Assert(Utils.MonitorRef != null, "Monitor ref is not set.");Utils.MonitorRef.Log(message, level); #elseif (level > LogLevel.Debug){Debug.Assert(MonitorRef != null, "Monitor ref is not set.");MonitorRef.Log(message, level);} #endif}public static bool Ensure(bool condition, string message){ #if DEBUGif (!condition){DebugLog($"Failed Ensure: {message}");} #endifreturn !!condition;}}2.I18n
多語言切換,下面是簡單的默認 en英文 json數據與 zh簡中 json數據
default.json
{"labels.single-price": "Single","labels.stack-price": "Stack" }zh.json
{"labels.single-price": "單價","labels.stack-price": "總計" }I18n工具類,可直接調用方法獲取對應語言翻譯后的文本內容
internal static class I18n {/*********** Fields*********//// <summary>Mod翻譯助手</summary>private static ITranslationHelper Translations;/*********** Public methods*********//// <summary>初始化</summary>/// <param name="translations">Mod翻譯助手</param>public static void Init(ITranslationHelper translations){I18n.Translations = translations;}/// <summary>獲取單價對應翻譯后的文本</summary>public static string Labels_SinglePrice(){return I18n.GetByKey("labels.single-price");}/// <summary>獲取總價對應翻譯后的文本</summary>public static string Labels_StackPrice(){return I18n.GetByKey("labels.stack-price");}/*********** Private methods*********//// <summary>通過KEY獲取翻譯后對應的文本</summary>/// <param name="key">JSON KEY</param>/// <param name="tokens">令牌,貌似沒發現如何用</param>private static Translation GetByKey(string key, object tokens = null){// 保證在讀取翻譯文件前從mod中獲取到設置的語言if (I18n.Translations == null)throw new InvalidOperationException($"You must call {nameof(I18n)}.{nameof(I18n.Init)} from the mod's entry method before reading translations.");return I18n.Translations.Get(key, tokens);} }二、繪制操作
下面舉一些簡單的實例,比如鼠標懸停物品上,新增一個文本提示框在鼠標左下方
1.物品單價提示
首先可設置強制出售物品種類數組,如果物品是不可出售的且種類不是強制可出售的那么就不會繼續向下進行方法
{// 強制可銷售物品種類數組"ForceSellable": [// 馬龍公會:adventure guild-28,-98,-97,-96,// 克林特鐵匠鋪:blacksmith-12,-2,-15,// 瑪尼動物店:Marnie's shop-18,-6,-5,-14,// 皮埃爾雜貨店:pierre's shop-81,-75,-79,-80,-74,-17,-18,-6,-26,-5,-14,-19,-7,-25,// 羅賓木匠鋪:Robin's shop-16,// 威利漁鋪:Willy's shop-4,-23,-21,-22] }1.在渲染菜單、渲染hud和游戲狀態更新時觸發3個對應的自定義事件 OnRenderedActiveMenu、OnRenderedHud、OnUpdateTicked
internal class DataModel {/// <summary>存放商店可出售的物品種類的實體類</summary>public HashSet<int> ForceSellable { get; set; } = new HashSet<int>(); } /// <summary>無法直接從游戲物品中獲得的強制可出售的物品種類</summary> private DataModel Data;public override void Entry(IModHelper helper) {...// 加載強制可出售物品this.Data = helper.Data.ReadJsonFile<DataModel>("assets/data.json") ?? new DataModel();this.Data.ForceSellable ??= new HashSet<int>();// 事件觸發helper.Events.Display.RenderedActiveMenu += this.OnRenderedActiveMenu;helper.Events.Display.RenderedHud += this.OnRenderedHud;helper.Events.GameLoop.UpdateTicked += this.OnUpdateTicked; }2.游戲狀態更新時,為全局定義的工具欄和工具欄槽變量賦值
/// <summary>緩存的工具欄實例</summary> private readonly PerScreen<Toolbar> Toolbar = new PerScreen<Toolbar>();/// <summary>緩存的工具欄槽</summary> private readonly PerScreen<IList<ClickableComponent>> ToolbarSlots = new PerScreen<IList<ClickableComponent>>();/// <summary>游戲狀態更新后觸發 (≈60次/秒).</summary> /// <param name="sender">當前事件對象</param> /// <param name="e">事件參數</param> private void OnUpdateTicked(object sender, UpdateTickedEventArgs e) {// Utils.DebugLog("SellPrice UpdateTicked", LogLevel.Info);if (e.IsOneSecond){if (Context.IsPlayerFree){this.Toolbar.Value = Game1.onScreenMenus.OfType<Toolbar>().FirstOrDefault();this.ToolbarSlots.Value = this.Toolbar.Value != null? this.Helper.Reflection.GetField<List<ClickableComponent>>(this.Toolbar.Value, "buttons").GetValue(): null;}else{this.Toolbar.Value = null;this.ToolbarSlots.Value = null;}} }3.渲染各個菜單時,從菜單中獲取懸停位置物品的信息
/// <summary>當打開一個菜單時,在渲染屏幕前繪制時觸發。確保 activeClickableMenu 不為空</summary> /// <param name="sender">當前事件對象</param> /// <param name="e">事件參數</param> private void OnRenderedActiveMenu(object sender, RenderedActiveMenuEventArgs e) {Utils.DebugLog("SellPrice RenderedActiveMenu", LogLevel.Info);Item item = this.GetItemFromMenu(Game1.activeClickableMenu);if (item == null)return;this.DrawPriceTooltip(Game1.spriteBatch, Game1.smallFont, item); }/// <summary>從菜單獲取懸停物品的信息</summary> /// <param name="menu">懸浮顯示的菜單</param> private Item GetItemFromMenu(IClickableMenu menu) {// 游戲菜單獲取if (menu is GameMenu gameMenu){Utils.DebugLog("SellPrice GetItemFromMenu", LogLevel.Info);IClickableMenu page = this.Helper.Reflection.GetField<List<IClickableMenu>>(gameMenu, "pages").GetValue()[gameMenu.currentTab];if (page is InventoryPage)return this.Helper.Reflection.GetField<Item>(page, "hoveredItem").GetValue();else if (page is CraftingPage)return this.Helper.Reflection.GetField<Item>(page, "hoverItem").GetValue();}// 資源菜單獲取else if (menu is MenuWithInventory inventoryMenu)return inventoryMenu.hoveredItem;return null; }4.渲染HUD時,從工具欄中獲取鼠標懸停位置物品的信息
/// <summary>在渲染到屏幕前繪制 HUD(工具欄、時鐘、天氣) 時觸發</summary> /// <param name="sender">當前事件對象</param> /// <param name="e">事件參數</param> private void OnRenderedHud(object sender, EventArgs e) {Utils.DebugLog("SellPrice RenderedHud", LogLevel.Info);if (!Context.IsPlayerFree)return;Item item = this.GetItemFromToolbar();if (item == null)return;this.DrawPriceTooltip(Game1.spriteBatch, Game1.smallFont, item); }/// <summary>從工具欄獲取懸停物品的信息</summary> private Item GetItemFromToolbar() {/*確保以下條件全都滿足再繼續獲取物品1.角色處于閑置狀態IsPlayerFree2.工具欄Value不為空3.工具欄中槽位不為空4.Hud處于顯示狀態*/if (!Context.IsPlayerFree || this.Toolbar?.Value == null || this.ToolbarSlots == null || !Game1.displayHUD)return null;// 查找懸停位置int x = Game1.getMouseX();int y = Game1.getMouseY();ClickableComponent hoveredSlot = this.ToolbarSlots.Value.FirstOrDefault(slot => slot.containsPoint(x, y));if (hoveredSlot == null)return null;// 獲取資源索引int index = this.ToolbarSlots.Value.IndexOf(hoveredSlot);if (index < 0 || index > Game1.player.Items.Count - 1)return null;Utils.DebugLog("SellPrice GetItemFromToolbar", LogLevel.Info);// 獲取懸停物品return Game1.player.Items[index]; }5.上面的方法中最后都會調用繪制單價提示框的方法 DrawPriceTooltip ,該方法是根據傳來的物品參數來獲取對應的價格,計算價格或總價,拼接文本,計算繪制長度,繪制提示框
/********* ** Fields *********/ /// <summary>硬幣圖標矩形</summary> private readonly Rectangle CoinSourceRect = new Rectangle(5, 69, 6, 6);/// <summary>工具提示框矩形</summary> private readonly Rectangle TooltipSourceRect = new Rectangle(0, 256, 60, 60);/// <summary>邊框像素大小</summary> private const int TooltipBorderSize = 15;/// <summary>提示框內邊距</summary> private const int Padding = 8;/// <summary>光標對于工具欄的偏移量</summary> private readonly Vector2 TooltipOffset = new Vector2(Game1.tileSize / 2);/// <summary>繪制商品單價與總價提示框</summary> /// <param name="spriteBatch">繪圖刷</param> /// <param name="font">繪制文本的字體</param> /// <param name="item">要顯示信息的物品</param> private void DrawPriceTooltip(SpriteBatch spriteBatch, SpriteFont font, Item item) {int stack = item.Stack;bool showStack = stack > 1;int? price = this.GetSellPrice(item);if (price == null)return;// 獲取全局設置的邊框、內邊距、硬幣尺寸、行高const int borderSize = LaSellPriceEntry.TooltipBorderSize;const int padding = LaSellPriceEntry.Padding;int coinSize = this.CoinSourceRect.Width * Game1.pixelZoom;int lineHeight = (int)font.MeasureString("X").Y;Vector2 offsetFromCursor = this.TooltipOffset;// 文本拼接string unitLabel = I18n.Labels_SinglePrice() + ":";string unitPrice = price.ToString();string stackLabel = I18n.Labels_StackPrice() + ":";string stackPrice = (price * stack).ToString();// 計算單價尺寸,總價尺寸,文本尺寸Vector2 unitPriceSize = font.MeasureString(unitPrice);Vector2 stackPriceSize = font.MeasureString(stackPrice);Vector2 labelSize = font.MeasureString(unitLabel);// 有總價的話,取最長的if (showStack)labelSize = new Vector2(Math.Max(labelSize.X, font.MeasureString(stackLabel).X), labelSize.Y * 2);// 計算提示框內容尺寸以及最外層尺寸Vector2 innerSize = new Vector2(labelSize.X + padding + Math.Max(unitPriceSize.X, showStack ? stackPriceSize.X : 0) + padding + coinSize, labelSize.Y);Vector2 outerSize = innerSize + new Vector2((borderSize + padding) * 2);// 根據鼠標計算位置//float x = Game1.getMouseX() - offsetFromCursor.X - outerSize.X;float x = Game1.getMouseX() - outerSize.X;float y = Game1.getMouseY() + offsetFromCursor.Y + borderSize;// 調整位置以適應屏幕Rectangle area = new Rectangle((int)x, (int)y, (int)outerSize.X, (int)outerSize.Y);if (area.Right > Game1.uiViewport.Width)x = Game1.uiViewport.Width - area.Width;if (area.Bottom > Game1.uiViewport.Height)y = Game1.uiViewport.Height - area.Height;// 繪制提示框IClickableMenu.drawTextureBox(spriteBatch, Game1.menuTexture, this.TooltipSourceRect, (int)x, (int)y, (int)outerSize.X, (int)outerSize.Y, Color.White);// 繪制硬幣與文本,如果showStack庫存大于1則繪制總價行硬幣與文本spriteBatch.Draw(Game1.debrisSpriteSheet, new Vector2(x + outerSize.X - borderSize - padding - coinSize, y + borderSize + padding), this.CoinSourceRect, Color.White, 0.0f, Vector2.Zero, Game1.pixelZoom, SpriteEffects.None, 1f);if (showStack)spriteBatch.Draw(Game1.debrisSpriteSheet, new Vector2(x + outerSize.X - borderSize - padding - coinSize, y + borderSize + padding + lineHeight), this.CoinSourceRect, Color.White, 0.0f, Vector2.Zero, Game1.pixelZoom, SpriteEffects.None, 1f);Utility.drawTextWithShadow(spriteBatch, unitLabel, font, new Vector2(x + borderSize + padding, y + borderSize + padding), Game1.textColor);Utility.drawTextWithShadow(spriteBatch, unitPrice, font, new Vector2(x + outerSize.X - borderSize - padding - coinSize - padding - unitPriceSize.X, y + borderSize + padding), Game1.textColor);if (showStack){Utility.drawTextWithShadow(spriteBatch, stackLabel, font, new Vector2(x + borderSize + padding, y + borderSize + padding + lineHeight), Game1.textColor);Utility.drawTextWithShadow(spriteBatch, stackPrice, font, new Vector2(x + outerSize.X - borderSize - padding - coinSize - padding - stackPriceSize.X, y + borderSize + padding + lineHeight), Game1.textColor);} }/// <summary>從物品信息中獲取售價</summary> /// <param name="item">物品</param> /// <returns>返回售價, 或者不能售出返回 <c>null</c> </returns> private int? GetSellPrice(Item item) {// 跳過不可出售物品if (!this.CanBeSold(item))return null;// 使用sv中Utility公用類方法獲取出售價格// return ((i is Object) ? (i as Object).sellToStorePrice(-1L) : (i.salePrice() / 2)) * ((!countStack) ? 1 : i.Stack);int price = Utility.getSellToStorePriceOfItem(item, countStack: false);return price >= 0 ? price : null as int?; }/// <summary>判斷是否可出售</summary> /// <param name="item">物品</param> private bool CanBeSold(Item item) {// 物品類型是否正確并且可被出售 or 是否包含在強制出售數組中(根據物品分類判斷)return(item is SObject obj && obj.canBeShipped())|| this.Data.ForceSellable.Contains(item.Category); }總結
C#yyds啊
總結
以上是生活随笔為你收集整理的SMAPI Mod制作思路的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: apk 连接服务器 修改,修改apk连接
- 下一篇: 使用横截面回归和时间序列回归进行回测