Card System
The Popcorning Guinea Pig engine includes a comprehensive 3D card system designed for building card games, deck builders, and other card-based interactions. The system provides Card3D components for individual cards and CardPile3D for managing collections of cards, with full support for 3D positioning, animations, and interactive sections.
Overview
The card system offers:
- 3D Card Rendering: Full 3D card models with front and back textures
- Interactive Sections: Define clickable regions on cards with tooltips
- Card Piles: Manage collections of cards with shuffling and drawing
- Nine-Slice Support: Scalable card borders and decorations
- Drag & Drop: Built-in support for card manipulation
- Visual Effects: Highlights, animations, and particle effects
- PGPSCN Integration: Define cards declaratively in scene files
Core Components
Card3D
The Card3D class represents individual cards in 3D space:
using PopcorningGuineaPig.Engine.UI;
// Create a 3D card
var card = new Card3D(
frontTexture, // Front face texture
backTexture, // Back face texture
position, // World position
new Vector2(1.5f, 2.0f), // Card dimensions
0.05f // Card thickness
);
// Configure the card
card.SetFaceUp(true);
card.SetPosition(new Vector3(0, 0.5f, 0));
card.SetRotation(new Vector3(0, 45, 0));
CardPile3D
The CardPile3D class manages collections of cards:
using PopcorningGuineaPig.Engine.UI;
// Create a card pile
var drawPile = new CardPile3D(
new Vector3(-4, 0.2f, -2.0f), // Position
Vector3.Zero, // Rotation
0.01f // Stack offset per card
);
// Add cards to the pile
drawPile.AddCard(card, animate: true);
// Draw cards from the pile
Card3D? drawnCard = drawPile.DrawCard(animate: true);
// Shuffle the pile
drawPile.Shuffle();
Card Data Structure
Define card properties using the CardData class:
public enum CardSuit
{
Hearts,
Diamonds,
Clubs,
Spades
}
public enum CardValue
{
Ace = 1,
Two = 2,
// ... through King = 13
}
public class CardData
{
public CardSuit Suit { get; }
public CardValue Value { get; }
public CardData(CardSuit suit, CardValue value)
{
Suit = suit;
Value = value;
}
public override string ToString()
{
return $"{Value} of {Suit}";
}
}
Interactive Sections
Cards can have defined interactive regions with tooltips and click handling:
// Add interactive sections to a card
card.AddSection("title", new Rectangle(0.1f, 0.05f, 0.8f, 0.15f)); // Top section
card.AddSection("artwork", new Rectangle(0.15f, 0.25f, 0.7f, 0.4f)); // Center section
card.AddSection("loot", new Rectangle(0.1f, 0.75f, 0.8f, 0.2f)); // Bottom section
// Check if mouse is over a section
Vector2 mousePos = Raylib.GetMousePosition();
string sectionName;
if (card.IsPointOver(mousePos, camera, out sectionName))
{
// Handle section hover/click
switch (sectionName)
{
case "title":
ShowTooltip("Card Title - Shows the name of this card");
break;
case "artwork":
ShowTooltip("Card Artwork - The main illustration");
break;
case "loot":
ShowTooltip("Loot Icons - Rewards for playing this card");
break;
}
}
Nine-Slice Support
Cards support nine-slice textures for scalable borders:
// Set nine-slice borders (10 pixel corners)
card.SetFrontNineSlice(10);
card.SetBackNineSlice(10);
// Nine-slice allows cards to scale while maintaining border proportions
card.SetSize(new Vector2(2.0f, 3.0f)); // Scale up while preserving borders
Card Highlighting
Cards can be highlighted for visual feedback:
// Highlight a card
card.SetHighlight(true, Color.Yellow, 1.0f);
// Remove highlight
card.SetHighlight(false, Color.Blank);
// Pulse highlight effect
card.SetHighlight(true, Color.Gold, Mathf.Sin(time * 5.0f) * 0.5f + 0.5f);
Drag and Drop System
Implement card drag and drop with mouse interaction:
private Card3D? currentCard;
private bool isDragging;
private Vector3 dragOffset;
private void HandleMouseInput()
{
Vector2 mousePos = Raylib.GetMousePosition();
// Start dragging
if (Raylib.IsMouseButtonPressed(MouseButton.Left))
{
foreach (var card in playerHand)
{
if (card.IsPointOver(mousePos, camera, out string section))
{
currentCard = card;
isDragging = true;
// Calculate drag offset
Vector3 cardScreenPos = RaycastHelper.WorldToScreen(
currentCard.GetPosition(), camera);
dragOffset = new Vector3(
mousePos.X - cardScreenPos.X,
mousePos.Y - cardScreenPos.Y,
0);
break;
}
}
}
// Update drag position
if (isDragging && currentCard != null)
{
Ray ray = Raylib.GetScreenToWorldRay(mousePos, camera);
Vector3 planeNormal = Vector3.UnitY;
Vector3 planePoint = new Vector3(0, 0.5f, 0);
float denominator = Vector3.Dot(ray.Direction, planeNormal);
if (Math.Abs(denominator) > 0.0001f)
{
float t = Vector3.Dot(planePoint - ray.Position, planeNormal) / denominator;
if (t >= 0)
{
Vector3 intersectionPoint = ray.Position + ray.Direction * t;
currentCard.SetPosition(new Vector3(
intersectionPoint.X, 0.5f, intersectionPoint.Z));
}
}
}
// End dragging
if (Raylib.IsMouseButtonReleased(MouseButton.Left) && isDragging)
{
isDragging = false;
// Check drop targets
if (IsDroppedOnDiscardPile())
{
DiscardCard(currentCard);
}
else
{
ReturnToHand(currentCard);
}
currentCard = null;
}
}
Card Animations
Cards support smooth movement and rotation animations:
// Animate card to a position
card.MoveTo(targetPosition, duration: 0.5f);
// Animate card rotation
card.RotateTo(targetRotation, duration: 0.3f);
// Flip animation
card.FlipCard(duration: 0.2f);
// Custom animation with easing
card.AnimateProperty(
property: "position",
targetValue: targetPosition,
duration: 1.0f,
easing: EasingType.EaseOutBounce
);
Creating a Complete Card Game
Here's a complete example of setting up a card game scene:
public class CardGameScene : Scene
{
private Camera3D camera;
private CardPile3D drawPile;
private CardPile3D discardPile;
private List<Card3D> playerHand;
private Card3D? currentCard;
private bool isDragging;
public override void Load()
{
// Initialize camera
camera = new Camera3D
{
Position = new Vector3(0, 10, 0),
Target = new Vector3(0, 0, 0),
Up = new Vector3(0, 0, -1),
FovY = 45,
Projection = CameraProjection.Perspective
};
// Create card piles
drawPile = new CardPile3D(new Vector3(-4, 0.2f, -2.0f), Vector3.Zero, 0.01f);
discardPile = new CardPile3D(new Vector3(4, 0.2f, -2.0f), Vector3.Zero, 0.01f);
playerHand = new List<Card3D>();
// Create and shuffle deck
CreateDeck();
drawPile.Shuffle();
// Draw initial hand
for (int i = 0; i < 5; i++)
{
DrawCard();
}
}
private void CreateDeck()
{
foreach (CardSuit suit in Enum.GetValues(typeof(CardSuit)))
{
foreach (CardValue value in Enum.GetValues(typeof(CardValue)))
{
var cardData = new CardData(suit, value);
var frontTexture = GenerateCardTexture(suit, value);
var backTexture = LoadCardBackTexture();
var card = new Card3D(
frontTexture, backTexture,
Vector3.Zero,
new Vector2(1.5f, 2.0f),
0.05f);
// Add interactive sections
card.AddSection("title", new Rectangle(0.1f, 0.05f, 0.8f, 0.15f));
card.AddSection("artwork", new Rectangle(0.15f, 0.25f, 0.7f, 0.4f));
card.AddSection("stats", new Rectangle(0.1f, 0.75f, 0.8f, 0.2f));
card.SetFaceUp(false);
drawPile.AddCard(card, false);
}
}
}
private void DrawCard()
{
Card3D? card = drawPile.DrawCard(true);
if (card != null)
{
playerHand.Add(card);
// Position in hand
int cardIndex = playerHand.Count - 1;
float xPos = -2.0f + cardIndex * 0.8f;
card.SetPosition(new Vector3(xPos, 0.5f, 2.0f));
card.SetFaceUp(true);
}
}
public override void Update(double deltaTime)
{
float dt = (float)deltaTime;
// Update card piles
drawPile.Update(dt);
discardPile.Update(dt);
// Update player hand
foreach (var card in playerHand)
{
card.Update(dt);
}
// Handle input
HandleMouseInput();
}
public override void Render(int width, int height)
{
Raylib.ClearBackground(Color.DarkGreen);
Raylib.BeginMode3D(camera);
{
// Draw table/background
Raylib.DrawCube(Vector3.Zero, 10.0f, 0.1f, 6.0f, Color.Green);
// Draw card piles
drawPile.Draw(camera);
discardPile.Draw(camera);
// Draw player hand
foreach (var card in playerHand)
{
card.Draw(camera);
}
}
Raylib.EndMode3D();
// Draw UI
DrawUI();
}
private void DrawUI()
{
Raylib.DrawText($"Cards: {drawPile.GetCardCount()}", 20, 20, 20, Color.White);
Raylib.DrawText("Click draw pile for cards", 20, 50, 16, Color.LightGray);
Raylib.DrawText("Drag cards to discard", 20, 70, 16, Color.LightGray);
}
}
PGPSCN Integration
Define cards in PGPSCN scene files for data-driven card games:
{
"version": "1.0",
"sceneType": "3d",
"assets": {
"textures": {
"card_back": "assets/cards/card_back.png",
"card_hearts": "assets/cards/hearts.png",
"card_spades": "assets/cards/spades.png"
}
},
"ui_elements": [
{
"id": "draw_pile",
"type": "CardPile3D",
"properties": {
"position": {"x": -4, "y": 0.2, "z": -2},
"rotation": {"x": 0, "y": 0, "z": 0},
"stackOffset": 0.01,
"maxVisible": 1
}
},
{
"id": "discard_pile",
"type": "CardPile3D",
"properties": {
"position": {"x": 4, "y": 0.2, "z": -2},
"rotation": {"x": 0, "y": 0, "z": 0},
"stackOffset": 0.01,
"maxVisible": 3
}
},
{
"id": "player_card_1",
"type": "Card3D",
"properties": {
"frontTexture": "card_hearts",
"backTexture": "card_back",
"position": {"x": -2, "y": 0.5, "z": 2},
"size": {"width": 1.5, "height": 2.0},
"thickness": 0.05,
"faceUp": true
},
"sections": [
{
"name": "title",
"rect": {"x": 0.1, "y": 0.05, "width": 0.8, "height": 0.15},
"tooltip": "Card title and cost"
},
{
"name": "artwork",
"rect": {"x": 0.15, "y": 0.25, "width": 0.7, "height": 0.4},
"tooltip": "Main card artwork"
}
],
"actions": [
{
"trigger": "onClick",
"section": "title",
"command": {
"type": "scene_method",
"method": "ShowCardDetails"
}
}
]
}
]
}
Texture Generation
The engine includes utilities for generating card textures programmatically:
using PopcorningGuineaPig.Engine;
// Generate card back texture
Texture2D cardBack = TextureGenerator.GenerateCardBackTexture(
width: 200,
height: 300,
color: new Color(0, 0, 128, 255)
);
// Generate suit-specific card front
Texture2D cardFront = TextureGenerator.GenerateCardFrontTexture(
width: 200,
height: 300,
suit: CardSuit.Hearts,
value: CardValue.Ace
);
// Generate table texture
Texture2D tableTexture = TextureGenerator.GenerateTableTexture(1024, 1024);
Best Practices
Performance Optimization
- Texture Management: Reuse textures across similar cards
- Update Batching: Only update cards that are moving or changing
- Culling: Don't render cards outside the camera view
- LOD: Use lower detail models for distant cards
// Efficient texture reuse
private Dictionary<string, Texture2D> cardTextures = new();
private Texture2D GetCardTexture(CardSuit suit, CardValue value)
{
string key = $"{suit}_{value}";
if (!cardTextures.ContainsKey(key))
{
cardTextures[key] = TextureGenerator.GenerateCardFrontTexture(
200, 300, suit, value);
}
return cardTextures[key];
}
// Selective updates
foreach (var card in activeCards.Where(c => c.IsMoving || c.IsDirty))
{
card.Update(deltaTime);
}
Memory Management
public override void Unload()
{
// Clean up card textures
foreach (var texture in cardTextures.Values)
{
Raylib.UnloadTexture(texture);
}
cardTextures.Clear();
// Unload card piles
drawPile.Unload();
discardPile.Unload();
// Clean up individual cards
foreach (var card in playerHand)
{
card.Unload();
}
}
Game State Management
[Serializable]
public class GameState
{
public List<CardData> DrawPile { get; set; }
public List<CardData> DiscardPile { get; set; }
public List<CardData> PlayerHand { get; set; }
public int Score { get; set; }
}
public void SaveGame()
{
var state = new GameState
{
DrawPile = drawPile.GetCards().Select(c => c.Data).ToList(),
DiscardPile = discardPile.GetCards().Select(c => c.Data).ToList(),
PlayerHand = playerHand.Select(c => c.Data).ToList(),
Score = currentScore
};
File.WriteAllText("savegame.json", JsonSerializer.Serialize(state));
}
Visual Effects
Particle Effects
// Card draw effect
var drawEffect = new ParticleEffect
{
Position = drawPile.GetPosition(),
Color = Color.Gold,
ParticleCount = 10,
Duration = 0.5f,
Spread = 0.2f
};
drawEffect.Trigger();
// Card discard effect
var discardEffect = new ParticleEffect
{
Position = card.GetPosition(),
Color = Color.Red,
ParticleCount = 15,
Duration = 0.8f,
Velocity = new Vector3(0, 2, 0)
};
discardEffect.Trigger();
Card Trails
// Add trail effect to moving cards
public class CardTrail
{
private List<Vector3> positions = new();
private float maxLength = 10;
public void Update(Vector3 currentPosition)
{
positions.Add(currentPosition);
if (positions.Count > maxLength)
{
positions.RemoveAt(0);
}
}
public void Draw()
{
for (int i = 1; i < positions.Count; i++)
{
float alpha = (float)i / positions.Count;
Color trailColor = new Color(255, 255, 255, (int)(alpha * 100));
Raylib.DrawLine3D(positions[i-1], positions[i], trailColor);
}
}
}
Advanced Features
Card Stacking and Layering
public class CardLayer
{
public List<Card3D> Cards { get; } = new();
public int Depth { get; set; }
public bool Interactive { get; set; } = true;
public void AddCard(Card3D card, int index = -1)
{
if (index == -1)
Cards.Add(card);
else
Cards.Insert(index, card);
UpdateCardDepths();
}
private void UpdateCardDepths()
{
for (int i = 0; i < Cards.Count; i++)
{
var pos = Cards[i].GetPosition();
Cards[i].SetPosition(new Vector3(pos.X, pos.Y + i * 0.01f, pos.Z));
}
}
}
Custom Card Types
public class SpellCard : Card3D
{
public int ManaCost { get; set; }
public string SpellEffect { get; set; }
public SpellCard(Texture2D front, Texture2D back, Vector3 position,
int manaCost, string effect)
: base(front, back, position, new Vector2(1.5f, 2.0f), 0.05f)
{
ManaCost = manaCost;
SpellEffect = effect;
// Add spell-specific sections
AddSection("mana_cost", new Rectangle(0.8f, 0.05f, 0.15f, 0.15f));
AddSection("spell_text", new Rectangle(0.1f, 0.7f, 0.8f, 0.25f));
}
public override void OnSectionClicked(string sectionName)
{
base.OnSectionClicked(sectionName);
if (sectionName == "mana_cost")
{
ShowManaCostTooltip();
}
else if (sectionName == "spell_text")
{
ShowSpellEffectDetails();
}
}
}
The card system in Popcorning Guinea Pig provides a robust foundation for creating engaging card-based games with rich 3D interactions, smooth animations, and flexible customization options.
Further Reading
- 3D Scenes - 3D rendering and camera systems
- PGPSCN Format - Declarative scene definition
- UI Components - Base UI system
- Asset Registry - Asset management